Skip to content
Snippets Groups Projects
Commit d89b0c87 authored by srosse's avatar srosse
Browse files

OO-929: remove version on UserCourseInfos (move from optimistic locking to...

OO-929: remove version on UserCourseInfos (move from optimistic locking to last commit win), rewrite the update method, refactor the UserCourseInfos mapping to use annotations
parent e9f7a9c9
No related branches found
No related tags found
No related merge requests found
......@@ -48,7 +48,6 @@
<mapping-file>org/olat/course/nodes/projectbroker/datamodel/Project.hbm.xml</mapping-file>
<mapping-file>org/olat/course/nodes/projectbroker/datamodel/ProjectBroker.hbm.xml</mapping-file>
<mapping-file>org/olat/course/assessment/model/UserEfficiencyStatementImpl.hbm.xml</mapping-file>
<mapping-file>org/olat/course/assessment/model/UserCourseInfosImpl.hbm.xml</mapping-file>
<mapping-file>org/olat/modules/fo/ForumImpl.hbm.xml</mapping-file>
<mapping-file>org/olat/modules/fo/MessageImpl.hbm.xml</mapping-file>
<mapping-file>org/olat/modules/fo/ReadMessage.hbm.xml</mapping-file>
......@@ -92,6 +91,7 @@
<class>org.olat.core.dispatcher.mapper.model.PersistedMapper</class>
<class>org.olat.core.commons.services.taskexecutor.model.PersistentTask</class>
<class>org.olat.core.commons.services.taskexecutor.model.PersistentTaskModifier</class>
<class>org.olat.course.assessment.model.UserCourseInfosImpl</class>
<class>org.olat.group.model.BusinessGroupParticipantViewImpl</class>
<class>org.olat.group.model.BusinessGroupOwnerViewImpl</class>
<class>org.olat.group.model.BusinessGroupLazyImpl</class>
......
......@@ -39,7 +39,7 @@ public interface UserCourseInformationsManager {
public List<UserCourseInformations> getUserCourseInformations(Identity identity, List<OLATResource> resources);
public void updateUserCourseInformations(Long courseResId, Identity identity);
public void updateUserCourseInformations(Long courseResId, Identity identity, boolean strict);
public Date getInitialLaunchDate(Long courseResourceId, Identity identity);
......
......@@ -65,8 +65,9 @@ public class UserCourseInformationsManagerImpl extends BasicManager implements U
try {
StringBuilder sb = new StringBuilder();
sb.append("select infos from ").append(UserCourseInfosImpl.class.getName()).append(" as infos ")
.append(" inner join infos.resource as resource")
.append(" where infos.identity.key=:identityKey and resource.resId=:resId and resource.resName='CourseModule'");
.append(" inner join fetch infos.resource as resource")
.append(" inner join infos.identity as identity")
.append(" where identity.key=:identityKey and resource.resId=:resId and resource.resName='CourseModule'");
List<UserCourseInfosImpl> infoList = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), UserCourseInfosImpl.class)
......@@ -93,8 +94,8 @@ public class UserCourseInformationsManagerImpl extends BasicManager implements U
try {
StringBuilder sb = new StringBuilder();
sb.append("select infos from ").append(UserCourseInfosImpl.class.getName()).append(" as infos ")
.append(" inner join fetch infos.resource as resource")
.append(" inner join fetch infos.identity as identity")
.append(" inner join fetch infos.resource as resource")
.append(" inner join infos.identity as identity")
.append(" where identity.key=:identityKey and resource.key in (:resKeys)");
List<Long> resourceKeys = PersistenceHelper.toKeys(resources);
......@@ -117,35 +118,94 @@ public class UserCourseInformationsManagerImpl extends BasicManager implements U
* @return
*/
@Override
public void updateUserCourseInformations(final Long courseResourceableId, final Identity identity) {
OLATResourceable lockRes = OresHelper.createOLATResourceableInstance("CourseLaunchDate::Identity", identity.getKey());
CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(lockRes, new SyncerExecutor(){
@Override
public void execute() {
try {
UserCourseInfosImpl infos = getUserCourseInformations(courseResourceableId, identity);
if(infos == null) {
OLATResource courseResource = resourceManager.findResourceable(courseResourceableId, "CourseModule");
infos = new UserCourseInfosImpl();
infos.setIdentity(identity);
infos.setInitialLaunch(new Date());
infos.setLastModified(new Date());
infos.setRecentLaunch(new Date());
infos.setVisit(1);
infos.setResource(courseResource);
dbInstance.getCurrentEntityManager().persist(infos);
} else if(needUpdate(infos)) {
dbInstance.getCurrentEntityManager().refresh(infos);
infos.setVisit(infos.getVisit() + 1);
infos.setRecentLaunch(new Date());
infos.setLastModified(new Date());
infos = dbInstance.getCurrentEntityManager().merge(infos);
public void updateUserCourseInformations(final Long courseResourceableId, final Identity identity, final boolean strict) {
UltraLightInfos ulInfos = getUserCourseInformationsKey(courseResourceableId, identity);
if(ulInfos == null) {
OLATResourceable lockRes = OresHelper.createOLATResourceableInstance("CourseLaunchDate::Identity", identity.getKey());
CoordinatorManager.getInstance().getCoordinator().getSyncer().doInSync(lockRes, new SyncerExecutor(){
@Override
public void execute() {
try {
UltraLightInfos ulInfos = getUserCourseInformationsKey(courseResourceableId, identity);
if(ulInfos == null) {
OLATResource courseResource = resourceManager.findResourceable(courseResourceableId, "CourseModule");
UserCourseInfosImpl infos = new UserCourseInfosImpl();
infos.setIdentity(identity);
infos.setCreationDate(new Date());
infos.setInitialLaunch(new Date());
infos.setLastModified(new Date());
infos.setRecentLaunch(new Date());
infos.setVisit(1);
infos.setResource(courseResource);
dbInstance.getCurrentEntityManager().persist(infos);
} else if(strict || needUpdate(ulInfos)) {
UserCourseInfosImpl infos = loadById(ulInfos.getKey());
if(infos != null) {
infos.setVisit(infos.getVisit() + 1);
infos.setRecentLaunch(new Date());
infos.setLastModified(new Date());
infos = dbInstance.getCurrentEntityManager().merge(infos);
}
}
} catch (Exception e) {
logError("Cannot update course informations for: " + identity + " from " + identity, e);
}
} catch (Exception e) {
logError("Cannot update course informations for: " + identity + " from " + identity, e);
}
});
} else if(strict || needUpdate(ulInfos)) {
UserCourseInfosImpl infos = loadById(ulInfos.getKey());
if(infos != null) {
infos.setVisit(infos.getVisit() + 1);
infos.setRecentLaunch(new Date());
infos.setLastModified(new Date());
infos = dbInstance.getCurrentEntityManager().merge(infos);
}
});
}
}
private UserCourseInfosImpl loadById(Long id) {
try {
String sb = "select infos from usercourseinfos as infos where infos.key=:key";
TypedQuery<UserCourseInfosImpl> query = dbInstance.getCurrentEntityManager()
.createQuery(sb, UserCourseInfosImpl.class)
.setParameter("key", id);
List<UserCourseInfosImpl> infoList = query.getResultList();
if(infoList.isEmpty()) {
return null;
}
return infoList.get(0);
} catch (Exception e) {
logError("Cannot retrieve course informations for: " + id, e);
return null;
}
}
private UltraLightInfos getUserCourseInformationsKey(Long courseResourceId, Identity identity) {
try {
StringBuilder sb = new StringBuilder();
sb.append("select infos.key, infos.lastModified from ").append(UserCourseInfosImpl.class.getName()).append(" as infos ")
.append(" inner join infos.resource as resource")
.append(" inner join infos.identity as identity")
.append(" where identity.key=:identityKey and resource.resId=:resId and resource.resName='CourseModule'");
List<Object[]> infoList = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Object[].class)
.setParameter("identityKey", identity.getKey())
.setParameter("resId", courseResourceId)
.getResultList();
if(infoList.isEmpty()) {
return null;
}
Object[] infos = infoList.get(0);
return new UltraLightInfos((Long)infos[0], (Date)infos[1]);
} catch (Exception e) {
logError("Cannot retrieve course informations for: " + identity + " from " + identity, e);
return null;
}
}
/**
......@@ -155,10 +215,10 @@ public class UserCourseInformationsManagerImpl extends BasicManager implements U
* opens a course several times.
* @return
*/
private final boolean needUpdate(UserCourseInfosImpl infos) {
private final boolean needUpdate(UltraLightInfos infos) {
Date lastModified = infos.getLastModified();
if(System.currentTimeMillis() - lastModified.getTime() < 60000) {
return false;
return true;
}
return true;
}
......@@ -173,7 +233,8 @@ public class UserCourseInformationsManagerImpl extends BasicManager implements U
StringBuilder sb = new StringBuilder();
sb.append("select infos.initialLaunch from ").append(UserCourseInfosImpl.class.getName()).append(" as infos ")
.append(" inner join infos.resource as resource")
.append(" where infos.identity.key=:identityKey and resource.resId=:resId and resource.resName='CourseModule'");
.append(" inner join infos.identity as identity")
.append(" where identity.key=:identityKey and resource.resId=:resId and resource.resName='CourseModule'");
List<Date> infoList = dbInstance.getCurrentEntityManager()
.createQuery(sb.toString(), Date.class)
......@@ -256,4 +317,21 @@ public class UserCourseInformationsManagerImpl extends BasicManager implements U
return -1;
}
}
private static class UltraLightInfos {
private final Long key;
private final Date lastModified;
public UltraLightInfos(Long key, Date lastModified) {
this.key = key;
this.lastModified = lastModified;
}
public Long getKey() {
return key;
}
public Date getLastModified() {
return lastModified;
}
}
}
\ No newline at end of file
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping default-lazy="false">
<class name="org.olat.course.assessment.model.UserCourseInfosImpl" table="o_as_user_course_infos">
<cache usage="transactional" />
<id name="key" type="long" column="id" unsaved-value="null">
<generator class="hilo"/>
</id>
<version name="version" access="field" column="version" type="int"/>
<property name="creationDate" column="creationdate" type="timestamp" />
<property name="lastModified" column="lastmodified" type="timestamp" />
<property name="initialLaunch" column="initiallaunchdate" type="timestamp" />
<property name="recentLaunch" column="recentlaunchdate" type="timestamp" />
<property name="visit" column="visit" type="int" />
<property name="timeSpend" column="timespend" type="long" />
<many-to-one name="resource"
column="fk_resource_id"
foreign-key="none"
class="org.olat.resource.OLATResourceImpl"
outer-join="true"
unique="false"
not-found="ignore"
cascade="none"/>
<many-to-one name="identity"
column="fk_identity"
foreign-key="cx_eff_statement_to_identity"
class="org.olat.basesecurity.IdentityImpl"
outer-join="true"
unique="false"
cascade="none"/>
</class>
</hibernate-mapping>
\ No newline at end of file
/**
* <a href="http://www.openolat.org">
$ * <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
......@@ -21,25 +21,84 @@ package org.olat.course.assessment.model;
import java.util.Date;
import org.olat.core.commons.persistence.PersistentObject;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import org.hibernate.annotations.GenericGenerator;
import org.olat.basesecurity.IdentityImpl;
import org.olat.core.id.Identity;
import org.olat.core.id.ModifiedInfo;
import org.olat.core.id.Persistable;
import org.olat.course.assessment.UserCourseInformations;
import org.olat.resource.OLATResource;
import org.olat.resource.OLATResourceImpl;
public class UserCourseInfosImpl extends PersistentObject implements UserCourseInformations, ModifiedInfo {
@Entity(name="usercourseinfos")
@Table(name="o_as_user_course_infos")
public class UserCourseInfosImpl implements UserCourseInformations, Persistable, ModifiedInfo {
private static final long serialVersionUID = -6933599547069673655L;
@Id
@GeneratedValue(generator = "system-uuid")
@GenericGenerator(name = "system-uuid", strategy = "hilo")
@Column(name="id", nullable=false, unique=true, insertable=true, updatable=false)
private Long key;
//de facto removing the optimistic locking
@Column(name="version", nullable=false, insertable=true, updatable=false)
private Integer version = 0;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="creationdate", nullable=false, insertable=true, updatable=false)
private Date creationDate;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="lastmodified", nullable=false, insertable=true, updatable=true)
private Date lastModified;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="initiallaunchdate", nullable=false, insertable=true, updatable=true)
private Date initialLaunch;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="recentlaunchdate", nullable=false, insertable=true, updatable=true)
private Date recentLaunch;
@Column(name="visit", nullable=false, insertable=true, updatable=true)
private int visit;
@Column(name="timespend", nullable=false, insertable=true, updatable=true)
private long timeSpend;
@ManyToOne(targetEntity=IdentityImpl.class,fetch=FetchType.LAZY,optional=false)
@JoinColumn(name="fk_identity", nullable=false, updatable=false)
private Identity identity;
@ManyToOne(targetEntity=OLATResourceImpl.class,fetch=FetchType.LAZY,optional=false)
@JoinColumn(name="fk_resource_id", nullable=false, updatable=false)
private OLATResource resource;
@Override
public Long getKey() {
return key;
}
public void setKey(Long key) {
this.key = key;
}
public Date getCreationDate() {
return creationDate;
}
public void setCreationDate(Date creationDate) {
this.creationDate = creationDate;
}
@Override
public Date getLastModified() {
return lastModified;
......@@ -107,6 +166,11 @@ public class UserCourseInfosImpl extends PersistentObject implements UserCourseI
public int hashCode() {
return getKey() == null ? 9271 : getKey().hashCode();
}
@Override
public boolean equalsByPersistableKey(Persistable persistable) {
return equals(persistable);
}
@Override
public boolean equals(Object obj) {
......
......@@ -344,8 +344,8 @@ public class RunMainController extends MainLayoutBasicController implements Gene
}
private void setLaunchDates(final Identity identity) {
UserCourseInformationsManager efficiencyStatementManager = CoreSpringFactory.getImpl(UserCourseInformationsManager.class);
efficiencyStatementManager.updateUserCourseInformations(uce.getCourseEnvironment().getCourseResourceableId(), getIdentity());
UserCourseInformationsManager userCourseInfoMgr = CoreSpringFactory.getImpl(UserCourseInformationsManager.class);
userCourseInfoMgr.updateUserCourseInformations(uce.getCourseEnvironment().getCourseResourceableId(), getIdentity(), false);
}
/**
......
......@@ -20,15 +20,20 @@
package org.olat.course.assessment.manager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Assert;
import org.junit.Test;
import org.olat.core.commons.persistence.DB;
import org.olat.core.id.Identity;
import org.olat.core.logging.OLog;
import org.olat.core.logging.Tracing;
import org.olat.course.ICourse;
import org.olat.course.assessment.UserCourseInformations;
import org.olat.restapi.repository.course.CoursesWebService;
......@@ -43,6 +48,8 @@ import org.springframework.beans.factory.annotation.Autowired;
*
*/
public class UserCourseInformationsManagerTest extends OlatTestCase {
private static final OLog log = Tracing.createLoggerFor(UserCourseInformationsManagerTest.class);
@Autowired
private DB dbInstance;
......@@ -50,12 +57,12 @@ public class UserCourseInformationsManagerTest extends OlatTestCase {
private UserCourseInformationsManager userCourseInformationsManager;
@Test
public void createUpdateCourseInfos() {
public void createUpdateCourseInfos_create() {
Identity user = JunitTestHelper.createAndPersistIdentityAsUser("user-launch-1-" + UUID.randomUUID().toString());
ICourse course = CoursesWebService.createEmptyCourse(user, "course-launch-dates", "course long name", null);
dbInstance.commitAndCloseSession();
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user);
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user, true);
dbInstance.commitAndCloseSession();
UserCourseInformations infos = userCourseInformationsManager.getUserCourseInformations(course.getResourceableId(), user);
......@@ -71,13 +78,30 @@ public class UserCourseInformationsManagerTest extends OlatTestCase {
Assert.assertEquals(course.getResourceableTypeName(), infos.getResource().getResourceableTypeName());
}
@Test
public void createUpdateCourseInfos_updateToo() {
Identity user = JunitTestHelper.createAndPersistIdentityAsUser("user-launch-1-" + UUID.randomUUID().toString());
ICourse course = CoursesWebService.createEmptyCourse(user, "course-launch-dates", "course long name", null);
dbInstance.commitAndCloseSession();
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user, true);
dbInstance.commitAndCloseSession();
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user, true);
dbInstance.commitAndCloseSession();
UserCourseInformations infos = userCourseInformationsManager.getUserCourseInformations(course.getResourceableId(), user);
Assert.assertNotNull(infos);
Assert.assertNotNull(infos.getIdentity());
}
@Test
public void getInitialLaunchDate() {
Identity user = JunitTestHelper.createAndPersistIdentityAsUser("user-launch-2-" + UUID.randomUUID().toString());
ICourse course = CoursesWebService.createEmptyCourse(user, "course-launch-dates", "course long name", null);
dbInstance.commitAndCloseSession();
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user);
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user, true);
dbInstance.commitAndCloseSession();
Date launchDate = userCourseInformationsManager.getInitialLaunchDate(course.getResourceableId(), user);
......@@ -91,8 +115,8 @@ public class UserCourseInformationsManagerTest extends OlatTestCase {
ICourse course = CoursesWebService.createEmptyCourse(user1, "course-launch-dates", "course long name", null);
dbInstance.commitAndCloseSession();
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user1);
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user2);
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user1, true);
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user2, true);
dbInstance.commitAndCloseSession();
List<Identity> users = new ArrayList<Identity>();
......@@ -107,4 +131,105 @@ public class UserCourseInformationsManagerTest extends OlatTestCase {
Assert.assertTrue(launchDates.containsKey(user2.getKey()));
Assert.assertNotNull(launchDates.get(user2.getKey()));
}
/**
* This test is to analyze a red screen
*/
@Test
public void updateInitialLaunchDates_loop() {
Identity user = JunitTestHelper.createAndPersistIdentityAsUser("user-launch-5-" + UUID.randomUUID().toString());
ICourse course = CoursesWebService.createEmptyCourse(user, "course-launch-dates", "course long name", null);
dbInstance.commitAndCloseSession();
for(int i=0; i<10; i++) {
userCourseInformationsManager.updateUserCourseInformations(course.getResourceableId(), user, true);
}
dbInstance.commitAndCloseSession();
List<Identity> users = Collections.singletonList(user);
Map<Long,Date> launchDates = userCourseInformationsManager.getInitialLaunchDates(course.getResourceableId(), users);
Assert.assertNotNull(launchDates);
Assert.assertEquals(1, launchDates.size());
Assert.assertTrue(launchDates.containsKey(user.getKey()));
Assert.assertNotNull(launchDates.get(user.getKey()));
}
/**
* This test is to analyze a red screen
*/
@Test
public void updateInitialLaunchDates_concurrent() {
Identity user = JunitTestHelper.createAndPersistIdentityAsUser("user-launch-concurrent-6-" + UUID.randomUUID().toString());
ICourse course = CoursesWebService.createEmptyCourse(user, "course-concurrent-launch-dates", "course long name", null);
dbInstance.commitAndCloseSession();
final int numThreads = 20;
CountDownLatch latch = new CountDownLatch(numThreads);
UpdateThread[] threads = new UpdateThread[numThreads];
for(int i=0; i<threads.length;i++) {
threads[i] = new UpdateThread(user, course.getResourceableId(), userCourseInformationsManager, latch, dbInstance);
}
for(int i=0; i<threads.length;i++) {
threads[i].start();
}
try {
latch.await(120, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.error("", e);
}
int countErrors = 0;
for(int i=0; i<threads.length;i++) {
countErrors += threads[i].getErrors();
}
Assert.assertEquals(0, countErrors);
}
private static class UpdateThread extends Thread {
private final DB db;
private final CountDownLatch latch;
private final UserCourseInformationsManager uciManager;
private final Long courseResourceableId;
private final Identity user;
private int errors = 0;
public UpdateThread(Identity user, Long courseResourceableId,
UserCourseInformationsManager uciManager, CountDownLatch latch, DB db) {
this.user = user;
this.courseResourceableId = courseResourceableId;
this.uciManager = uciManager;
this.latch = latch;
this.db = db;
}
public int getErrors() {
return errors;
}
@Override
public void run() {
try {
Thread.sleep(10);
for(int i=0; i<25;i++) {
uciManager.updateUserCourseInformations(courseResourceableId, user, true);
uciManager.getUserCourseInformations(courseResourceableId, user);
uciManager.updateUserCourseInformations(courseResourceableId, user, true);
db.commitAndCloseSession();
}
} catch (Exception e) {
log.error("", e);
errors++;
} finally {
latch.countDown();
}
}
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment