[Notice] Announcing the End of Demo Server [Read me]
Yi EungJun 2014-05-19
issue: Fix an exception while posting many issues
Reason: Every issue posting requires updating of project.lastIssueNumber
and it may occur OptimisticLockexception if the project.lastIssueNumber
has been changed already by other issue posting.

Solution: In project.increaseLastIssueNumber(), do not update self but
the project just fetched from database by Project.find.byId()

To make the solution works correctly, I did following works as well:

1. Add getter or setter to access the lastIssueNumber field. The
   increaseLastIssueNumber method cannot update lastIssueNumber directly
   because Ebean does not set the project to dirty even if the method
   modified the field.  When a private field is accessed directly
   (Ebean's "Field Access") in a static method or the other instance's
   method, Ebean does not call preSetter and/or preGetter so the
   instance will not be set to dirty.  I guess it is a bug of Ebean.

2. Refresh project before updating it, as the fixing of
   test/controllers/IssueAppTest.java in this commit. Posting an issue
   makes every project instance to have smaller lastIssueNumber than the
   actual value stored in database.

3. Make project.increaseLastIssueNumber static. It looks unnatural that
   non-static project.increaseLastIsseNumber method does not increase
   lastIssueNumber field of self.

4. Apply these fixes to Posting too.
@f3e011267b2a21a4be6dd06c1258d11ffc532345
app/models/Issue.java
--- app/models/Issue.java
+++ app/models/Issue.java
@@ -103,11 +103,11 @@
      */
     @Override
     protected Long increaseNumber() {
-        return project.increaseLastIssueNumber();
+        return Project.increaseLastIssueNumber(project.id);
     }
 
     protected void fixLastNumber() {
-        project.fixLastIssueNumber();
+        Project.fixLastIssueNumber(project.id);
     }
 
     public String assigneeName() {
app/models/Posting.java
--- app/models/Posting.java
+++ app/models/Posting.java
@@ -31,11 +31,11 @@
      */
     @Override
     protected Long increaseNumber() {
-        return project.increaseLastPostingNumber();
+        return Project.increaseLastPostingNumber(project.id);
     }
 
     protected void fixLastNumber() {
-        project.fixLastPostingNumber();
+        Project.fixLastPostingNumber(project.id);
     }
 
     /**
app/models/Project.java
--- app/models/Project.java
+++ app/models/Project.java
@@ -305,45 +305,85 @@
         return null;
     }
 
-    @Transactional
-    public Long increaseLastIssueNumber() {
-        if (this.issues != null && lastIssueNumber < this.issues.size() ){
-            fixLastIssueNumber();
-        }
-
-        lastIssueNumber++;
-        update();
+    private Long getLastIssueNumber() {
         return lastIssueNumber;
     }
 
-    public void fixLastIssueNumber() {
-        Issue issue = Issue.finder.where().eq("project.id", id).order().desc("number").findList().get(0);
-        issue.refresh();
-        if (issue.number == null) {
-            lastIssueNumber = 0;
-        } else {
-            lastIssueNumber = issue.number;
-        }
+    private void setLastIssueNumber(Long number) {
+        lastIssueNumber = number;
     }
 
-    @Transactional
-    public Long increaseLastPostingNumber() {
-        if (this.posts != null && lastPostingNumber < this.posts.size() ){
-            fixLastPostingNumber();
-        }
-        lastPostingNumber++;
-        update();
+    private Long getLastPostingNumber() {
         return lastPostingNumber;
     }
 
-    public void fixLastPostingNumber() {
-        Posting posting = Posting.finder.where().eq("project.id", id).order().desc("number").findList().get(0);
-        posting.refresh();
-        if (posting.number == null) {
-            lastPostingNumber = 0;
-        } else {
-            lastPostingNumber = posting.number;
+    private void setLastPostingNumber(Long number) {
+        lastPostingNumber = number;
+    }
+
+    /**
+     * 마지막 이슈번호를 증가시킨다.
+     *
+     * 이슈 추가시 사용한다.
+     *
+     * @return {@code lastIssueNumber}
+     */
+    public static Long increaseLastIssueNumber(Long projectId) {
+        Project project = find.byId(projectId);
+
+        if (project.issues != null && project.lastIssueNumber < project.issues.size() ){
+            fixLastIssueNumber(projectId);
         }
+
+        project.setLastIssueNumber(project.getLastIssueNumber() + 1);
+        project.update();
+
+        return project.lastIssueNumber;
+    }
+
+    public static void fixLastIssueNumber(Long projectId) {
+        Project project = find.byId(projectId);
+        project.refresh();
+
+        Issue issue = Issue.finder.where().eq("project.id", projectId).order().desc("number").findList().get(0);
+        issue.refresh();
+
+        if (issue.number == null) {
+            project.lastIssueNumber = 0;
+        } else {
+            project.lastIssueNumber = issue.number;
+        }
+
+        project.update();
+    }
+
+    public static Long increaseLastPostingNumber(Long projectId) {
+        Project project = find.byId(projectId);
+
+        if (project.posts != null && project.lastPostingNumber < project.posts.size() ){
+            fixLastPostingNumber(projectId);
+        }
+
+        project.setLastPostingNumber(project.getLastPostingNumber() + 1);
+        project.update();
+
+        return project.lastPostingNumber;
+    }
+
+    public static void fixLastPostingNumber(Long projectId) {
+        Project project = find.byId(projectId);
+        project.refresh();
+
+        Posting posting = Posting.finder.where().eq("project.id", projectId).order().desc("number").findList().get(0);
+        posting.refresh();
+
+        if (posting.number == null) {
+            project.lastPostingNumber = 0;
+        } else {
+            project.lastPostingNumber = posting.number;
+        }
+
+        project.update();
     }
 
     public Resource labelsAsResource() {
test/controllers/IssueAppTest.java
--- test/controllers/IssueAppTest.java
+++ test/controllers/IssueAppTest.java
@@ -156,6 +156,7 @@
     @Test
     public void editByNonmember() {
         // Given
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
@@ -215,6 +216,7 @@
     @Test
     public void deleteByNonmember() {
         // Given
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
@@ -274,6 +276,7 @@
     @Test
     public void postByAnonymous() {
         // Given
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
@@ -287,6 +290,7 @@
     @Test
     public void postByNonmember() {
         // Given
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
@@ -327,6 +331,7 @@
     @Test
     public void commentByAnonymous() {
         // Given
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
@@ -340,6 +345,7 @@
     @Test
     public void commentByNonmember() {
         // Given
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
@@ -388,6 +394,7 @@
     public void watch() {
         // Given
         Resource resource = issue.asResource();
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
@@ -443,6 +450,7 @@
     public void unwatch() {
         // Given
         Resource resource = issue.asResource();
+        project.refresh();
         project.setProjectScope(ProjectScope.PUBLIC);
         project.update();
 
test/models/IssueTest.java
--- test/models/IssueTest.java
+++ test/models/IssueTest.java
@@ -236,4 +236,20 @@
         event.created = DateUtils.parseDate(str, "yyyy-MM-dd");
         return event;
     }
+
+    @Test
+    public void optimisticLockException() {
+        Project project1 = Project.findByOwnerAndProjectName("yobi", "projectYobi");
+        Project project2 = Project.findByOwnerAndProjectName("yobi", "projectYobi");
+
+        issue = new Issue();
+        issue.setProject(project1);
+        issue.setTitle("a");
+        issue.save();
+
+        issue = new Issue();
+        issue.setProject(project2);
+        issue.setTitle("b");
+        issue.save();
+    }
 }
Add a comment
List