Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 41 additions & 10 deletions assignment04/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@
* Carleton University
* @version 1.0, January 07th, 2025
* @version 2.0, January 10th, 2026
*
* @author Lavji, F
* @version 2.1, March 14, 2026
*/

public class Agent implements Runnable {

private AssemblyTable assemblyTable; //The common table between Agent and Technicians
private AssemblyTable assemblyTable; //The common table between Agent and Technicians

// NEW --> Addition of Event Logger (Assignment04_Requirement01)
private final EventLogger logger = EventLogger.getInstance();

/**
* Constructor for Agent
Expand All @@ -24,25 +29,51 @@ public Agent(AssemblyTable t){
}

/**
* Method used when Agent thread is ran
* Method used when Agent thread is ran.
*/
public void run(){
Components components1, components2;

String name = Thread.currentThread().getName();
logger.logEvent(name, "THREAD_START");

System.out.println("[" + Thread.currentThread().getName() + "] Waiting to place first components on the table...");
while (this.assemblyTable.getDronesAssembled() != 20){ //Will loop until 20 drones have been assembled
//Randomly selects two different components

// Loop until MAX_DRONES have been assembled
while (this.assemblyTable.getDronesAssembled() != assemblyTable.getMaxDrones()) {
long startAttempt = System.currentTimeMillis();

// Randomly selects two different components
components1 = Components.getRandomComponent();
components2 = Components.getRandomComponent();
while (components1 == components2){ //If components are the same, select and new second component
components2 = Components.getRandomComponent();
}
this.assemblyTable.addComponents(components1, components2); //Places the two selected components on the table

// If components are the same, select and new second component
while (components1 == components2){ components2 = Components.getRandomComponent(); }
logger.logEvent(name, "COMPONENTS_SELECTED",
components1.toString() + ", " + components2.toString());

//Places the two selected components on the table
this.assemblyTable.addComponents(components1, components2);
logger.logEvent(name, "COMPONENTS_ADDED",
components1.toString() + ", " + components2.toString());

// Sleep for between 0 and 5 seconds before calculating n!
try {
logger.logEvent(name, "WORK_START");
Thread.sleep((int)(Math.random() * 3000));
logger.logEvent(name, "WORK_END");
} catch (InterruptedException e) {}

Comment on lines 61 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f "Agent.java" assignment04

Repository: fareenlavji/rtConcurrentSystems

Length of output: 98


🏁 Script executed:

cat -n assignment04/Agent.java

Repository: fareenlavji/rtConcurrentSystems

Length of output: 3793


Preserve interrupt status instead of ignoring it.

The empty catch block at line 65 consumes InterruptedException and clears the thread's interrupt status, preventing graceful shutdown or external thread cancellation. When Thread.sleep() is interrupted, the exception should be handled by either re-interrupting the thread to restore its interrupted state or exiting the operation.

Proposed fix
            try {
                logger.logEvent(name, "WORK_START");
                Thread.sleep((int)(Math.random() * 3000));
                logger.logEvent(name, "WORK_END");
-            } catch (InterruptedException e) {}
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                logger.logEvent(name, "THREAD_INTERRUPTED");
+                break;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assignment04/Agent.java` around lines 61 - 66, The catch block in Agent.java
currently swallows InterruptedException; update it to restore the thread's
interrupt status and stop/return from the current work iteration. Replace the
empty catch with code that calls Thread.currentThread().interrupt() and then
either return from the method or break out of the work loop (whichever fits the
surrounding logic in the Agent class/run method) so the interruption is
propagated and the thread can shut down gracefully; keep the
logger.logEvent(name, "WORK_END") handling consistent if needed.

// NEW --> Metric Analysis (Assignment04_Requirement03)
long endAttempt = System.currentTimeMillis();
long responseTime = endAttempt - startAttempt;
logger.logEventKV(name, "RESPONSE_TIME", "duration", String.valueOf(responseTime));
Comment on lines +56 to +70
}

//All drones have been assembled
System.out.println("[" + Thread.currentThread().getName() + "] 20 drones assembled, ending...");
logger.logEvent(name, "THREAD_END",
String.format("%d drones assembled, ending...", assemblyTable.getMaxDrones()));
System.out.println(String.format("[%s] %d drones assembled, ending...",
Thread.currentThread().getName(), assemblyTable.getMaxDrones()));
}
}
180 changes: 135 additions & 45 deletions assignment04/AssemblyTable.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,94 +12,132 @@
* Carleton University
* @version 1.0, January 07th, 2025
* @version 2.0, January 10th, 2026
*
* @author Lavji, F
* @version 2.1, March 14, 2026
*/
public class AssemblyTable {
private final int SIZE = 2; //Capacity of table
private Components[] components = new Components[SIZE]; //List of components on the table
private boolean tableFull = false; //True if there is at least 1 component on the table
private int dronesMade = 0; //Running total of drones assembled
private final int SIZE = 2; //Capacity of table
private static final int MAX_DRONES = 20;
Comment on lines +20 to +21
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider making SIZE a static final constant for consistency.

SIZE is declared as final but not static, while MAX_DRONES is static final. Since SIZE is a compile-time constant and doesn't vary per instance, it should also be static final for consistency and to avoid redundant memory allocation per instance.

♻️ Suggested fix
-    private final int SIZE = 2; //Capacity of table
+    private static final int SIZE = 2; //Capacity of table
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private final int SIZE = 2; //Capacity of table
private static final int MAX_DRONES = 20;
private static final int SIZE = 2; //Capacity of table
private static final int MAX_DRONES = 20;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assignment04/AssemblyTable.java` around lines 20 - 21, Make SIZE a
class-level constant like MAX_DRONES by changing its declaration to static
final; update the field named SIZE in AssemblyTable (currently "private final
int SIZE = 2;") to "private static final int SIZE = 2" so it becomes a
compile-time constant shared across instances and consistent with MAX_DRONES.

private Components[] components = new Components[SIZE]; //List of components on the table
private boolean tableFull = false; //True if there is at least 1 component on the table
private int dronesMade = 0; //Running total of drones assembled

// NEW --> Addition of Event Logger (Assignment04_Requirement01)
private final EventLogger logger = EventLogger.getInstance();

/**
* Method used to allow an Agent to place components on the table when table is empty
* Method used to allow an Agent to place components on the table when table is empty.
*
* @param components1 First component to be placed by Agent
* @param components2 Second component to be placed by Agent
*/
public synchronized void addComponents(Components components1, Components components2) {
while (tableFull) { //Makes agent wait until table is empty to place components
if (this.dronesMade == 20){ //Will exit if no more drones are required to be assembled
return;
}
//Make agent wait until table is empty to place components
while (tableFull) {
logger.waitStart(Thread.currentThread().getName());

//Exit if no more drones are required to be assembled
if (this.dronesMade == MAX_DRONES){ return; }

try {
wait(); //Tells agent to wait until notified
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (this.dronesMade == 20){ //Will exit if no more drones are required to be assembled
return;
logger.waitEnd(Thread.currentThread().getName());
} catch (InterruptedException e) { e.printStackTrace(); }
Comment on lines +38 to +46
}
Comment on lines +37 to 47
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unbalanced wait logging on early exit.

When dronesMade == MAX_DRONES at line 41, the method returns after waitStart() has been logged but before waitEnd() can be logged. This creates unbalanced log entries that could confuse the LogAnalyzer or produce misleading metrics.

🔧 Proposed fix
        while (tableFull) {
-           logger.waitStart(Thread.currentThread().getName());
-
            //Exit if no more drones are required to be assembled
-           if (this.dronesMade == MAX_DRONES){ return; }
+           if (this.dronesMade == MAX_DRONES) { return; }

            try {
+               logger.waitStart(Thread.currentThread().getName());
                wait(); //Tells agent to wait until notified
                logger.waitEnd(Thread.currentThread().getName());
            } catch (InterruptedException e) { e.printStackTrace(); }
        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assignment04/AssemblyTable.java` around lines 37 - 47, The code logs
logger.waitStart(...) then may return early when this.dronesMade == MAX_DRONES,
leaving logger.waitEnd(...) unpaired; to fix, move the early-exit check for
this.dronesMade == MAX_DRONES to before calling
logger.waitStart(Thread.currentThread().getName()) or, if you must keep the
order, ensure you call logger.waitEnd(Thread.currentThread().getName())
immediately before the return inside the while (tableFull) block so every
waitStart has a matching waitEnd; update the block containing while (tableFull),
logger.waitStart, the this.dronesMade == MAX_DRONES check, and logger.waitEnd
accordingly.


//Exit if no more drones are required to be assembled
if (this.dronesMade == MAX_DRONES){ return; }

//Components are placed on table
components[0] = components1;
components[1] = components2;
logger.logEventKV(Thread.currentThread().getName(), "PLACED_COMPONENTS",
"components", "[" + components1 + "," + components2 + "]",
"drones", String.valueOf(dronesMade));


// Random delay to simulate real scenario
try {
logger.logEvent(Thread.currentThread().getName(), "WORK_START");
Thread.sleep((int)(Math.random() * 1000));
logger.logEvent(Thread.currentThread().getName(), "WORK_END");
} catch (InterruptedException e) {}
Comment on lines +62 to 65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Restore interrupted status when catching InterruptedException.

Swallowing InterruptedException with an empty catch block loses the interrupt signal. Best practice is to restore the interrupt status so that higher-level code can detect the interruption.

♻️ Proposed fix
         try {
             logger.logEvent(Thread.currentThread().getName(), "WORK_START");
             Thread.sleep((int)(Math.random() * 1000));
             logger.logEvent(Thread.currentThread().getName(), "WORK_END");
-        } catch (InterruptedException e) {}
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
logger.logEvent(Thread.currentThread().getName(), "WORK_START");
Thread.sleep((int)(Math.random() * 1000));
logger.logEvent(Thread.currentThread().getName(), "WORK_END");
} catch (InterruptedException e) {}
logger.logEvent(Thread.currentThread().getName(), "WORK_START");
Thread.sleep((int)(Math.random() * 1000));
logger.logEvent(Thread.currentThread().getName(), "WORK_END");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assignment04/AssemblyTable.java` around lines 62 - 65, The catch block that
swallows InterruptedException in AssemblyTable.java (surrounding
Thread.sleep(...) and logger.logEvent calls) should restore the thread's
interrupted status instead of being empty; update the catch
(InterruptedException e) { ... } to call Thread.currentThread().interrupt() (and
optionally log the interruption via logger.logEvent or logger.error) so
higher-level code can detect the interruption while keeping the existing
logger.logEvent and Thread.sleep usage intact.


tableFull = true; //Table is now full
System.out.println("[" + Thread.currentThread().getName() + "] " + components1.toString() + " and " + components2.toString() + " placed on the table.");
notifyAll(); //Notify all Technicians that table is full
logger.logEventKV(Thread.currentThread().getName(), "TABLE_FULL",
"components", "[" + components1 + "," + components2 + "]",
"drones", String.valueOf(dronesMade));

System.out.println("[" + Thread.currentThread().getName() + "] "
+ components1.toString() + " and "
+ components2.toString() + " placed on the table.");

// Notify all Technicians that table is full
logger.logEvent(Thread.currentThread().getName(), "READY");
notifyAll();
}

/**
* Method used by Technicians to obtain components on table and assemble a drone
*
* @param components The component the Technician has an infinite supply of (Used to determine if Technician is eligible to take the components on the table)
*/
public synchronized void getComponents(Components components)
{
while (!tableFull || componentsContains(components)) { //Makes Technician wait until the table is full and until the two required components from the Agent is available
if (this.dronesMade == 20){ //If 20 drones have been assembled, do not assemble another
return;
}
public synchronized void getComponents(Components components) {

//Makes Technician wait until the table is full or until the two required components from the Agent is available
while (!canTake(components)) {
//If MAX_DRONES have been assembled, do not assemble another
if (this.dronesMade == MAX_DRONES){ return; }

Comment on lines +89 to +92
try {
logger.waitStart(Thread.currentThread().getName());
wait(); //Make the Technician wait until notified that new components are available
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.waitEnd(Thread.currentThread().getName());
} catch (InterruptedException e) { e.printStackTrace(); }
}

logger.logEventKV(Thread.currentThread().getName(), "PICKED_UP",
"components", "[" + this.components[0] + "," + this.components[1] + "]",
"drones", String.valueOf(dronesMade));


System.out.println("[" + Thread.currentThread().getName() + "] Drone assembled.");
System.out.println("[" + Thread.currentThread().getName() + "] Waiting for remaining components...");
this.dronesMade++; //Increase running total of drones assembled
logger.logEventKV(Thread.currentThread().getName(), "DRONE_ASSEMBLED", "drones", String.valueOf(dronesMade));
System.out.println("[Counter] Drones assembled: " + this.dronesMade);
System.out.println("--------------------------------------------------------------");
//Clear components and set table to empty
this.components[0] = null;
this.components[1] = null;
tableFull = false;
logger.logEventKV(Thread.currentThread().getName(), "TABLE_EMPTY", "drones", String.valueOf(dronesMade));

// Random delay to simulate real scenario
try {
logger.logEvent(Thread.currentThread().getName(), "WORK_START");
Thread.sleep((int)(Math.random() * 1000));
logger.logEvent(Thread.currentThread().getName(), "WORK_END");
} catch (InterruptedException e) {}
Comment on lines 118 to 122
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Restore interrupted status when catching InterruptedException.

Same issue as in addComponents — the interrupt signal is lost when InterruptedException is caught with an empty block.

♻️ Proposed fix
         try {
             logger.logEvent(Thread.currentThread().getName(), "WORK_START");
             Thread.sleep((int)(Math.random() * 1000));
             logger.logEvent(Thread.currentThread().getName(), "WORK_END");
-        } catch (InterruptedException e) {}
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
logger.logEvent(Thread.currentThread().getName(), "WORK_START");
Thread.sleep((int)(Math.random() * 1000));
logger.logEvent(Thread.currentThread().getName(), "WORK_END");
} catch (InterruptedException e) {}
try {
logger.logEvent(Thread.currentThread().getName(), "WORK_START");
Thread.sleep((int)(Math.random() * 1000));
logger.logEvent(Thread.currentThread().getName(), "WORK_END");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assignment04/AssemblyTable.java` around lines 118 - 122, The catch block
swallowing InterruptedException loses the thread's interrupted status; update
the catch for InterruptedException (around the Thread.sleep call that logs
WORK_START/WORK_END and uses logger.logEvent) to restore the interrupt by
calling Thread.currentThread().interrupt() (and optionally log the interruption
via logger.logEvent or logger.error) so the interruption is propagated
correctly.


notifyAll(); //Notify Technicians and Agent that components have changed
logger.logEvent(Thread.currentThread().getName(), "READY");
notifyAll(); //Notify Technicians and Agent that components have changed
}

/**
* Method used to check if the component given is one of the two components on the table
* Method used to check if the component given is one of the two components on the table.
*
* @param components Component from Technician (used to check if Technician can accept the components on the table)
* @return True if component is on the table, false otherwise
* @param techOwns Component from Technician (to check if Technician can accept the components on the table)
* @return True if component is on the table, false otherwise.
*/
private boolean componentsContains (Components components){
//If there are no components on the table, or one of the components on the table is the same as the component given from the Technician, return True; false otherwise
return (this.components[0] == null || this.components[1] == null || (this.components[0] == components || this.components[1] == components));
private boolean canTake(Components techOwns) {
if (!tableFull) return false;
if (this.components[0] == null || this.components[1] == null) return false;
return (this.components[0] != techOwns && this.components[1] != techOwns);
}


/**
* Getter method for getDronesMade.
*
Expand All @@ -109,6 +147,13 @@ public int getDronesAssembled(){
return this.dronesMade;
}

/**
* Returns the maximum number of drones needed for the job to complete.
*
* @return The maximum number of drones for job completion.
*/
public int getMaxDrones() { return MAX_DRONES; }

/**
* Method used to create new Technician threads; keeps consistency of naming conventions between threads
*
Expand All @@ -125,21 +170,66 @@ private static Thread makeNewTechnician(AssemblyTable t, Components i){
*
* @param args
*/
public static void main (String[] args){
public static void main(String[] args) {
// Threads for each Technician and the Agent
Thread techFrame, techProp, techCtrl, agent;

// Common table for all threads
AssemblyTable assemblyTable = new AssemblyTable();

// Start the logger (daemon flusher inside)
EventLogger logger = EventLogger.getInstance();


// Create file with standardized naming
String runId = java.time.LocalDateTime.now()
.format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"));
String logFile = "assembly_log_" + runId + ".txt";
logger.setLogFileName(logFile);
logger.setFlushIntervalMs(250L);

Thread TechnicianFrame, TechnicianPropulsion, TechnicianControl, agent; //Threads for each Technician and the Agent
AssemblyTable assemblyTable; //Table
logger.start(); // background daemon flusher
// System-wide lifecycle anchors for analyzer
logger.logEventKV("AssemblyLine", "SYSTEM_START", "drones", "0");
logger.logEventKV("AssemblyLine", "ASSEMBLY_TABLE_READY", "drones", "0");

assemblyTable = new AssemblyTable(); //Common Table for all Technicians and Agent
agent = new Thread(new Agent(assemblyTable), "Agent"); //Agent thread created
TechnicianFrame = makeNewTechnician(assemblyTable, Components.Frame); //Beans Technician created
TechnicianPropulsion = makeNewTechnician(assemblyTable, Components.PropulsionUnit); //Water Technician created
TechnicianControl = makeNewTechnician(assemblyTable, Components.ControlFirmware); //Sugar Technician created
// Create threads with clear names (you already do this in makeNewTechnician)
agent = new Thread(new Agent(assemblyTable), "Agent");
techFrame = makeNewTechnician(assemblyTable, Components.Frame);
techProp = makeNewTechnician(assemblyTable, Components.PropulsionUnit);
techCtrl = makeNewTechnician(assemblyTable, Components.ControlFirmware);

//Start all Technician and Agent threads
TechnicianFrame.start();
TechnicianPropulsion.start();
TechnicianControl.start();
// Check-in logs before starting threads
logger.logEventKV("AssemblyLine", "FRAME_TECH_CHECKIN", "drones", "0");
techFrame.start();
logger.logEventKV("AssemblyLine", "PROPULSION_TECH_CHECKIN", "drones", "0");
techProp.start();
logger.logEventKV("AssemblyLine", "CONTROL_TECH_CHECKIN", "drones", "0");
techCtrl.start();
logger.logEventKV("AssemblyLine", "AGENT_CHECKIN", "drones", "0");
agent.start();

// Wait for completion
try {
agent.join();
techFrame.join();
techProp.join();
techCtrl.join();
} catch (InterruptedException e) { e.printStackTrace(); }
Comment on lines +213 to +218
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle InterruptedException per-join to ensure all threads are awaited.

If an InterruptedException is thrown during one join(), the remaining threads won't be joined, potentially leaving them running when logger.stop() is called. Consider joining each thread individually or re-attempting joins after restoring the interrupt flag.

🔧 Proposed fix
         // Wait for completion
-        try {
-            agent.join();
-            techFrame.join();
-            techProp.join();
-            techCtrl.join();
-        } catch (InterruptedException e) { e.printStackTrace(); }
+        for (Thread t : new Thread[]{agent, techFrame, techProp, techCtrl}) {
+            try {
+                t.join();
+            } catch (InterruptedException e) {
+                Thread.currentThread().interrupt();
+                e.printStackTrace();
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@assignment04/AssemblyTable.java` around lines 213 - 218, The current single
try/catch around agent.join(), techFrame.join(), techProp.join(),
techCtrl.join() can abort joining remaining threads if an InterruptedException
occurs; change this to handle each join individually (e.g., wrap each join call
for agent, techFrame, techProp, techCtrl in its own try/catch and/or loop to
retry until joined) and if interrupted restore the interrupt flag with
Thread.currentThread().interrupt() so logger.stop() is not called before all
threads are joined; ensure every target thread is attempted to be joined
regardless of interruptions.


// Completion summary and SYSTEM_END
logger.logEventKV("AssemblyLine", "JOB_COMPLETED", "drones", String.valueOf(assemblyTable.getDronesAssembled()));
logger.logEventKV("AssemblyLine", "SYSTEM_END", "drones", String.valueOf(assemblyTable.getDronesAssembled()));

// Stop logger and flush remaining records
logger.stop();
System.out.println("All threads finished; system terminated.");


// Run the analyzer AFTER logs are fully flushed (prints to console AND writes metrics.txt)
try {
LogAnalyzer.main(new String[]{ logFile });
System.out.println("Metrics written to metrics.txt");
} catch (Exception e) { e.printStackTrace(); }
}
}
13 changes: 8 additions & 5 deletions assignment04/Components.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import java.util.concurrent.ThreadLocalRandom; // Thread - safe
/**
* Components enums
*
Expand All @@ -6,21 +7,23 @@
* Carleton University
* @version 1.0, January 07th, 2025
* @version 2.0, January 10th, 2026
*
* @author Lavji, F
* @version 2.1, March 14, 2026
*/

import java.util.Random;

public enum Components {
Frame,
PropulsionUnit,
ControlFirmware;

/**
* Pick a random value of the Components enum.
*
* @return a random Component.
*/
public static Components getRandomComponent() {
Random random = new Random();
return values()[random.nextInt(values().length)];
// Switched out with thread-safe Randomizer
int i = ThreadLocalRandom.current().nextInt(values().length);
return values()[i];
}
}
Loading