Enable concurrent commit processing

Resolved issues associated with processing concurrent commits, thereby
allowing for their concurrent handling.

Jira-Id: IT-103
Change-Id: I749bb5b9e02ff66ef3879e26ec6772282d479e78
Signed-off-by: Patrizio <patrizio.gelosi@amarulasolutions.com>
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java
index 15cf7b7..c06abd1 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/Module.java
@@ -2,7 +2,6 @@
 
 import com.google.gerrit.server.events.EventListener;
 import com.google.inject.AbstractModule;
-import com.google.inject.Singleton;
 import com.google.inject.multibindings.Multibinder;
 import com.googlesource.gerrit.plugins.chatgpt.listener.GerritListener;
 
@@ -11,6 +10,6 @@
     @Override
     protected void configure() {
         Multibinder<EventListener> eventListenerBinder = Multibinder.newSetBinder(binder(), EventListener.class);
-        eventListenerBinder.addBinding().to(GerritListener.class).in(Singleton.class);
+        eventListenerBinder.addBinding().to(GerritListener.class);
     }
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
index 1b624eb..d69b1ab 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/PatchSetReviewer.java
@@ -4,12 +4,8 @@
 import com.google.gson.JsonObject;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
-import com.googlesource.gerrit.plugins.chatgpt.client.GerritClient;
-import com.googlesource.gerrit.plugins.chatgpt.client.InlineCode;
-import com.googlesource.gerrit.plugins.chatgpt.client.OpenAiClient;
+import com.googlesource.gerrit.plugins.chatgpt.client.*;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.ChatGptSuggestionPoint;
-import com.googlesource.gerrit.plugins.chatgpt.client.FileDiffProcessed;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.GerritCodeRange;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
 import lombok.Setter;
@@ -18,10 +14,9 @@
 import java.lang.reflect.Type;
 import java.util.*;
 
-import static com.googlesource.gerrit.plugins.chatgpt.client.ReviewUtils.extractID;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.ReviewUtils.extractID;
 
 @Slf4j
-@Singleton
 public class PatchSetReviewer {
     private static final String SPLIT_REVIEW_MSG = "Too many changes. Please consider splitting into patches smaller " +
             "than %s lines for review.";
@@ -44,7 +39,7 @@
 
     public void review(Configuration config, String fullChangeId) throws Exception {
         reviewBatches = new ArrayList<>();
-        commentProperties = gerritClient.getCommentProperties();
+        commentProperties = gerritClient.getCommentProperties(fullChangeId);
         String patchSet = gerritClient.getPatchSet(fullChangeId, isCommentEvent);
         if (patchSet.isEmpty()) {
             log.info("No file to review has been found in the PatchSet");
@@ -55,7 +50,7 @@
         String reviewSuggestion = getReviewSuggestion(config, fullChangeId, patchSet);
         log.debug("ChatGPT response: {}", reviewSuggestion);
         if (isCommentEvent || config.getGptReviewByPoints()) {
-            retrieveReviewFromJson(reviewSuggestion);
+            retrieveReviewFromJson(reviewSuggestion, fullChangeId);
         }
         else {
             splitReviewIntoBatches(reviewSuggestion);
@@ -117,11 +112,11 @@
         return gerritCommentRange;
     }
 
-    private void retrieveReviewFromJson(String review) {
+    private void retrieveReviewFromJson(String review, String fullChangeId) {
         review = review.replaceAll("^`*(?:json)?\\s*|\\s*`+$", "");
         Type chatGptResponseListType = new TypeToken<List<ChatGptSuggestionPoint>>(){}.getType();
         List<ChatGptSuggestionPoint> reviewJson = gson.fromJson(review, chatGptResponseListType);
-        fileDiffsProcessed = gerritClient.getFileDiffsProcessed();
+        fileDiffsProcessed = gerritClient.getFileDiffsProcessed(fullChangeId);
         for (ChatGptSuggestionPoint suggestion : reviewJson) {
             HashMap<String, Object> batchMap = new HashMap<>();
             if (suggestion.getId() != null) {
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java
index 61291af..6c44c4c 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClient.java
@@ -2,44 +2,30 @@
 
 import com.google.gerrit.server.events.Event;
 import com.google.gson.JsonObject;
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
 import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
+import com.googlesource.gerrit.plugins.chatgpt.utils.SingletonManager;
 import lombok.extern.slf4j.Slf4j;
 
 import java.util.HashMap;
 import java.util.List;
 
-class GerritClientPatchSetInjector extends AbstractModule {
-    @Override
-    protected void configure() {
-        bind(GerritClientPatchSet.class).in(Singleton.class);
-    }
-}
-
-class GerritClientCommentsInjector extends AbstractModule {
-    @Override
-    protected void configure() {
-        bind(GerritClientComments.class).in(Singleton.class);
-    }
-}
-
 @Slf4j
 @Singleton
 public class GerritClient {
-    private static final Injector patchSetInjector = Guice.createInjector(new GerritClientPatchSetInjector());
-    private static final Injector commentsInjector = Guice.createInjector(new GerritClientCommentsInjector());
+    private static final String DEFAULT_CHANGE_ID = "DEFAULT_CHANGE_ID";
 
     private GerritClientPatchSet gerritClientPatchSet;
     private GerritClientComments gerritClientComments;
 
     public void initialize(Configuration config) {
-        gerritClientPatchSet = patchSetInjector.getInstance(GerritClientPatchSet.class);
-        gerritClientPatchSet.initialize(config);
-        gerritClientComments = commentsInjector.getInstance(GerritClientComments.class);
-        gerritClientComments.initialize(config);
+        initialize(config, DEFAULT_CHANGE_ID);
+    }
+
+    public void initialize(Configuration config, String fullChangeId) {
+        log.debug("Initializing client instances for change: {}", fullChangeId);
+        gerritClientPatchSet = SingletonManager.getInstance(GerritClientPatchSet.class, fullChangeId, config);
+        gerritClientComments = SingletonManager.getInstance(GerritClientComments.class, fullChangeId, config);
     }
 
     public String getPatchSet(String fullChangeId) throws Exception {
@@ -47,6 +33,7 @@
     }
 
     public String getPatchSet(String fullChangeId, boolean isCommentEvent) throws Exception {
+        updateGerritClientPatchSet(fullChangeId);
         return gerritClientPatchSet.getPatchSet(fullChangeId, isCommentEvent);
     }
 
@@ -58,24 +45,43 @@
         return gerritClientPatchSet.isDisabledTopic(topic);
     }
 
-    public HashMap<String, FileDiffProcessed> getFileDiffsProcessed() {
+    public HashMap<String, FileDiffProcessed> getFileDiffsProcessed(String fullChangeId) {
+        updateGerritClientPatchSet(fullChangeId);
         return gerritClientPatchSet.getFileDiffsProcessed();
     }
 
-    public List<JsonObject> getCommentProperties() {
+    public List<JsonObject> getCommentProperties(String fullChangeId) {
+        updateGerritClientComments(fullChangeId);
         return gerritClientComments.getCommentProperties();
     }
 
     public void postComments(String fullChangeId, List<HashMap<String, Object>> reviewBatches) throws Exception {
+        updateGerritClientComments(fullChangeId);
         gerritClientComments.postComments(fullChangeId, reviewBatches);
     }
 
     public boolean retrieveLastComments(Event event, String fullChangeId) {
+        updateGerritClientComments(fullChangeId);
         return gerritClientComments.retrieveLastComments(event, fullChangeId);
     }
 
     public String getUserPrompt() {
-        return gerritClientComments.getUserPrompt(getFileDiffsProcessed());
+        HashMap<String, FileDiffProcessed> fileDiffsProcessed = gerritClientPatchSet.getFileDiffsProcessed();
+        return gerritClientComments.getUserPrompt(fileDiffsProcessed);
+    }
+
+    public void destroy(String fullChangeId) {
+        log.debug("Destroying client instances for change: {}", fullChangeId);
+        SingletonManager.removeInstance(GerritClientPatchSet.class, fullChangeId);
+        SingletonManager.removeInstance(GerritClientComments.class, fullChangeId);
+    }
+
+    private void updateGerritClientPatchSet(String fullChangeId) {
+        gerritClientPatchSet = SingletonManager.getInstance(GerritClientPatchSet.class, fullChangeId);
+    }
+
+    private void updateGerritClientComments(String fullChangeId) {
+        gerritClientComments = SingletonManager.getInstance(GerritClientComments.class, fullChangeId);
     }
 
 }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientAccount.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientAccount.java
index 2d262d4..f8f3de3 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientAccount.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientAccount.java
@@ -13,6 +13,10 @@
 @Slf4j
 public class GerritClientAccount extends GerritClientBase {
 
+    public GerritClientAccount(Configuration config) {
+        super(config);
+    }
+
     private Optional<Integer> getAccountId(String authorUsername) {
         URI uri = URI.create(config.getGerritAuthBaseUrl()
                 + UriResourceLocator.gerritAccountIdUri(authorUsername));
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientBase.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientBase.java
index 41b906a..3eb9ed9 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientBase.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientBase.java
@@ -26,7 +26,7 @@
     protected HashMap<String, FileDiffProcessed> fileDiffsProcessed = new HashMap<>();
     protected Configuration config;
 
-    public void initialize(Configuration config) {
+    public GerritClientBase(Configuration config) {
         this.config = config;
         config.resetDynamicConfiguration();
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientComments.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientComments.java
index 801affd..d719bfe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientComments.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientComments.java
@@ -7,7 +7,6 @@
 import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
-import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.ChatGptRequestPoint;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
 import lombok.Getter;
@@ -24,11 +23,10 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import static com.googlesource.gerrit.plugins.chatgpt.client.ReviewUtils.getTimeStamp;
+import static com.googlesource.gerrit.plugins.chatgpt.utils.ReviewUtils.getTimeStamp;
 import static java.net.HttpURLConnection.HTTP_OK;
 
 @Slf4j
-@Singleton
 public class GerritClientComments extends GerritClientAccount {
     private static final Integer MAX_SECS_GAP_BETWEEN_EVENT_AND_COMMENT = 2;
     private static final String BULLET_POINT = "* ";
@@ -39,8 +37,8 @@
     @Getter
     protected List<JsonObject> commentProperties;
 
-    public void initialize(Configuration config) {
-        super.initialize(config);
+    public GerritClientComments(Configuration config) {
+        super(config);
         commentProperties  = new ArrayList<>();
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientPatchSet.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientPatchSet.java
index e440cd3..768798d 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientPatchSet.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/GerritClientPatchSet.java
@@ -4,7 +4,6 @@
 import com.google.gson.GsonBuilder;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
-import com.google.inject.Singleton;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.InputFileDiff;
 import com.googlesource.gerrit.plugins.chatgpt.client.model.OutputFileDiff;
 import com.googlesource.gerrit.plugins.chatgpt.config.Configuration;
@@ -14,17 +13,15 @@
 import java.util.*;
 
 @Slf4j
-@Singleton
 public class GerritClientPatchSet extends GerritClientAccount {
     private final Gson gson = new GsonBuilder()
             .disableHtmlEscaping()
             .create();
-
+    private final List<String> diffs;
     private boolean isCommitMessage;
-    private List<String> diffs;
 
-    public void initialize(Configuration config) {
-        super.initialize(config);
+    public GerritClientPatchSet(Configuration config) {
+        super(config);
         diffs = new ArrayList<>();
     }
 
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java
index e9a5872..882e942 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/OpenAiClient.java
@@ -20,6 +20,7 @@
 import java.net.http.HttpResponse;
 import java.util.List;
 import java.util.Optional;
+import java.util.concurrent.ThreadLocalRandom;
 
 @Slf4j
 @Singleton
@@ -102,6 +103,9 @@
                 .messages(messages)
                 .temperature(config.getGptTemperature())
                 .stream(config.getGptStreamOutput() && !isCommentEvent)
+                // Seed value is Utilized to prevent ChatGPT from mixing up separate API calls that occur in close
+                // temporal proximity.
+                .seed(ThreadLocalRandom.current().nextInt())
                 .build();
 
         return gson.toJson(chatCompletionRequest);
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/model/ChatCompletionRequest.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/model/ChatCompletionRequest.java
index 9817f80..f167afe 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/model/ChatCompletionRequest.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/model/ChatCompletionRequest.java
@@ -11,6 +11,7 @@
     private String model;
     private boolean stream;
     private double temperature;
+    private int seed;
     private List<Message> messages;
 
     @Data
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
index a5fe208..51ec03a 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/listener/EventListenerHandler.java
@@ -131,37 +131,48 @@
         return true;
     }
 
-    public void handleEvent(Configuration config, Event event) {
-        this.config = config;
-        PatchSetEvent patchSetEvent = (PatchSetEvent) event;
+    private boolean preprocessEvent(Event event, String fullChangeId, Project.NameKey projectNameKey) {
         String eventType = Optional.ofNullable(event.getType()).orElse("");
         log.info("Event type {}", eventType);
-        Project.NameKey projectNameKey = patchSetEvent.getProjectNameKey();
-        BranchNameKey branchNameKey = patchSetEvent.getBranchNameKey();
-        Change.Key changeKey = patchSetEvent.getChangeKey();
-        String fullChangeId = buildFullChangeId(projectNameKey, branchNameKey, changeKey);
-
-        gerritClient.initialize(config);
+        PatchSetEvent patchSetEvent = (PatchSetEvent) event;
 
         if (!isReviewEnabled(patchSetEvent, projectNameKey)) {
-            return;
+            return false;
         }
         switch (eventType) {
             case "patchset-created":
                 if (!isPatchSetReviewEnabled(patchSetEvent)) {
-                    return;
+                    return false;
                 }
                 reviewer.setCommentEvent(false);
                 break;
             case "comment-added":
                 if (!gerritClient.retrieveLastComments(event, fullChangeId)) {
                     log.info("No comments found for review");
-                    return;
+                    return false;
                 }
                 reviewer.setCommentEvent(true);
                 break;
             default:
-                return;
+                return false;
+        }
+
+        return true;
+    }
+
+    public void handleEvent(Configuration config, Event event) {
+        this.config = config;
+        PatchSetEvent patchSetEvent = (PatchSetEvent) event;
+        Project.NameKey projectNameKey = patchSetEvent.getProjectNameKey();
+        BranchNameKey branchNameKey = patchSetEvent.getBranchNameKey();
+        Change.Key changeKey = patchSetEvent.getChangeKey();
+        String fullChangeId = buildFullChangeId(projectNameKey, branchNameKey, changeKey);
+
+        gerritClient.initialize(config, fullChangeId);
+
+        if (!preprocessEvent(event, fullChangeId, projectNameKey)) {
+            gerritClient.destroy(fullChangeId);
+            return;
         }
 
         // Execute the potentially time-consuming operation asynchronously
@@ -175,6 +186,8 @@
                 if (e instanceof InterruptedException) {
                     Thread.currentThread().interrupt();
                 }
+            } finally {
+                gerritClient.destroy(fullChangeId);
             }
         }, executorService);
     }
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/ReviewUtils.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/ReviewUtils.java
similarity index 94%
rename from src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/ReviewUtils.java
rename to src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/ReviewUtils.java
index a3d7998..d27fdb8 100644
--- a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/client/ReviewUtils.java
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/ReviewUtils.java
@@ -1,4 +1,4 @@
-package com.googlesource.gerrit.plugins.chatgpt.client;
+package com.googlesource.gerrit.plugins.chatgpt.utils;
 
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
diff --git a/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/SingletonManager.java b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/SingletonManager.java
new file mode 100644
index 0000000..7d15226
--- /dev/null
+++ b/src/main/java/com/googlesource/gerrit/plugins/chatgpt/utils/SingletonManager.java
@@ -0,0 +1,41 @@
+package com.googlesource.gerrit.plugins.chatgpt.utils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class SingletonManager {
+    private static final Map<String, Object> instances = new HashMap<>();
+
+    private static String getInstanceKey(Class<?> clazz, String id) {
+        return clazz.getName() + ":" + id;
+    }
+
+    public static synchronized <T> T getInstance(Class<T> clazz, String id, Object... constructorArgs) {
+        String key = getInstanceKey(clazz, id);
+        if (!instances.containsKey(key)) {
+            try {
+                // Use reflection to invoke constructor with arguments
+                T instance;
+                if (constructorArgs == null || constructorArgs.length == 0) {
+                    instance = clazz.getDeclaredConstructor().newInstance();
+                } else {
+                    Class<?>[] argClasses = new Class[constructorArgs.length];
+                    for (int i = 0; i < constructorArgs.length; i++) {
+                        argClasses[i] = constructorArgs[i].getClass();
+                    }
+                    instance = clazz.getDeclaredConstructor(argClasses).newInstance(constructorArgs);
+                }
+                instances.put(key, instance);
+                return instance;
+            } catch (Exception e) {
+                throw new RuntimeException("Error creating instance for class: " + clazz.getName(), e);
+            }
+        }
+        return clazz.cast(instances.get(key));
+    }
+
+    public static synchronized void removeInstance(Class<?> clazz, String id) {
+        instances.remove(getInstanceKey(clazz, id));
+    }
+
+}