[JBoss Cache: Core Edition] - Re: on child insert parent, child collection updated in DB b
by dukehoops
Here's the test class:
/*
| * To change this template, choose Tools | Templates
| * and open the template in the editor.
| */
| package com.doppelganger.service.userservice;
|
| import com.doppelganger.domain.User;
| import com.doppelganger.domain.UserSession;
| import com.doppelganger.test.AbstractFunctionalTest;
| import com.doppelganger.test.TestEntityFactory;
| import com.doppelganger.test.service.DgTxTemplateAdapter;
| import com.doppelganger.test.service.TransactionCallbackWithException;
| import java.sql.Timestamp;
| import java.util.HashSet;
| import java.util.Set;
| import java.util.UUID;
| import java.util.concurrent.ExecutorService;
| import java.util.concurrent.Executors;
| import java.util.concurrent.TimeUnit;
| import net.eviltwinstudios.common.CommonUtils;
| import org.springframework.beans.factory.annotation.Autowired;
| import org.springframework.transaction.TransactionStatus;
|
| /**
| * test cache consistency when many users login/read/logout in a loop
| * @author nikita
| */
| public class MultiuserLoginTest extends AbstractFunctionalTest {
|
| final private int USER_COUNT = 4;
| final private int ITERATION_COUNT = 20;
| final private long THINK_TIME_MILLIS = 0;
| final private long LAUNCH_INTERVAL_MILLIS = 10;
| @Autowired
| private DgTxTemplateAdapter txTemplate;
| final private ExecutorService pool = Executors.newFixedThreadPool(USER_COUNT);
| final private Set<String> LTESTER_USERNAMES = new HashSet<String>();
|
| @Override
| protected void onSetUp() throws Exception {
| super.onSetUp();
|
| for (int i = 0; i < USER_COUNT; i++) {
| LTESTER_USERNAMES.add(TestEntityFactory.LOADTESTUSER_PREFIX + (100000 + i));
| }
|
| }
|
| public void testManyUsers() throws InterruptedException {
|
| Set<LtesterUserRunner> runners = new HashSet<LtesterUserRunner>();
| for (String userName : LTESTER_USERNAMES) {
| LtesterUserRunner r = new LtesterUserRunner(userName);
| runners.add(r);
| pool.execute(r);
| Thread.sleep(LAUNCH_INTERVAL_MILLIS); //rampup
| }
|
| assertEquals("not all user threads launched", USER_COUNT, runners.size());
|
| getLogger().info("all clients launched");
|
| pool.shutdown();
| boolean finishedInTime = pool.awaitTermination(5, TimeUnit.MINUTES);
|
| assertTrue("test took too long", finishedInTime);
|
| //check whether all runners suceeded
| for (LtesterUserRunner r : runners) {
| assertEquals("runner for user=" + r.getUserName() + " did not complete all iterations; cause=" +
| CommonUtils.stackTraceToString(r.getCauseOfFailure()),
| ITERATION_COUNT,
| r.getCompletedIterations());
| }
| }
|
| /**
| * login Ltester user into environment and check cache state
| * @param ltesterUserName
| */
| public void login(final String ltesterUserName) throws Exception {
|
|
| //add userSession
| final UserSession us1 = (UserSession) txTemplate.executeWithException(new TransactionCallbackWithException() {
|
| public Object doInTransactionWithException(TransactionStatus status) throws Exception {
|
| //add user session, throw exception if one does not exist
| final User usr = getUserDao().findByNameChecked(ltesterUserName);
|
| UserSession userSession = new UserSession(usr);
| userSession.setToken(UUID.randomUUID().toString());
|
| userSession.setLoginTime(new Timestamp(System.currentTimeMillis()));
| userSession.resetSessionTimeout(); //ok here
|
| //attach userSession to User
| usr.initCurrentUserSession(userSession);
|
| return userSession;
|
| }
| });
|
| //check that we can get at userSession
| final UserSession us2 = getCurrentUserSession(ltesterUserName);
|
| if (us2 == null) {
| throw new IllegalStateException("userSession null after successful login");
| }
|
| if (!us1.equals(us2)) {
| throw new IllegalStateException("us1 != us2");
| }
| }
|
| /**
| * read UserSessions of all test participants
| */
| public void readUserSessionsOfTesters() throws Exception {
| txTemplate.executeWithException(new TransactionCallbackWithException() {
|
| public Object doInTransactionWithException(TransactionStatus status) throws Exception {
|
| for (String userName : LTESTER_USERNAMES) {
| final User usr = getUserDao().findByNameChecked(userName);
| usr.getCurrentUserSession(); //read user session
| }
| return null;
| }
| });
| }
|
| /**
| *
| * @param ltesterUserName
| * @return current userSession or null
| */
| private UserSession getCurrentUserSession(final String ltesterUserName) throws Exception {
|
| return (UserSession) txTemplate.executeWithException(new TransactionCallbackWithException() {
|
| public Object doInTransactionWithException(TransactionStatus status) throws Exception {
|
| //add user session, throw exception if one does not exist
| final User usr = getUserDao().findByNameChecked(ltesterUserName);
|
| return usr.getCurrentUserSession();
| }
| });
|
| }
|
| public void logout(final String ltesterUserName) throws Exception {
|
| //remove userSession
| txTemplate.executeWithException(new TransactionCallbackWithException() {
|
| public Object doInTransactionWithException(TransactionStatus status) throws Exception {
|
| //remove user session; throw exception if one didn't exist
| final User usr = getUserDao().findByNameChecked(ltesterUserName);
|
| if (usr.getCurrentUserSession() == null) {
| throw new IllegalStateException("current userSession is null *before* logout; user=" + ltesterUserName);
| }
| usr.removeCurrentUserSession();
|
| return null;
| }
| });
|
| //check that we can no longer get at userSession
| UserSession us = getCurrentUserSession(ltesterUserName);
|
| if (us != null) {
| throw new IllegalStateException("userSession remains after logout: us=" + us);
| }
| }
|
| class LtesterUserRunner implements Runnable {
|
| final private String userName;
| private int completedIterations = 0;
| private Throwable causeOfFailure;
|
| public LtesterUserRunner(final String usrName) {
| userName = usrName;
| }
|
| public void run() {
| try {
| for (int i = 0; i < ITERATION_COUNT; i++) {
| login(userName);
| //loginWithTestService(userName);
| //loginWithService(userName);
| think();
|
| //get users
| //getFriends2(userName);
| readUserSessionsOfTesters();
| think();
|
| logout(userName);
| think();
|
| ++completedIterations;
| }
|
| } catch (Throwable t) {
| this.causeOfFailure = t;
| }
| }
|
| /**
| * @return the userName
| */
| public String getUserName() {
| return userName;
| }
|
| /**
| * @return the success
| */
| public boolean isSuccess() {
| return ITERATION_COUNT == getCompletedIterations();
| }
|
| /**
| * @return the completedIterations
| */
| public int getCompletedIterations() {
| return completedIterations;
| }
|
| /**
| * @return the causeOfFailure
| */
| public Throwable getCauseOfFailure() {
| return causeOfFailure;
| }
| }
|
| private void think() {
| try {
| Thread.sleep(THINK_TIME_MILLIS);
| } catch (InterruptedException ex) {
| throw new RuntimeException("sleep interrupted", ex);
| }
| }
|
| }
|
and relevant Domain objects:
| @Entity
| @org.hibernate.annotations.Entity(dynamicUpdate = true, dynamicInsert = true)
| @Table(name = "users")
| @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
| public class User extends GeneratedHiLoKeyEntity {
|
| private Set<UserSession> userSessions = new HashSet<UserSession>();
|
| /**
| * @return the userSessions
| */
| @OneToMany(mappedBy = "user")
| @Cascade({org.hibernate.annotations.CascadeType.ALL, org.hibernate.annotations.CascadeType.DELETE_ORPHAN})
| @LazyCollection(LazyCollectionOption.TRUE)
| @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
| private Set<UserSession> getUserSessions() {
| return userSessions;
| }
|
| /**
| * @param userSessions the userSessions to set
| */
| private void setUserSessions(Set<UserSession> userSessions) {
| this.userSessions = userSessions;
| }
|
| @Transient
| public UserSession getCurrentUserSession() {
| final Set<UserSession> sessions = getUserSessions();
| return sessions.isEmpty() ? null : sessions.iterator().next();
| }
|
| /**
| * convenience method for linking User and currentUserSession
| */
| public void initCurrentUserSession(UserSession aUserSession) {
| assert aUserSession != null;
|
| if (!getUserSessions().isEmpty()) {
| throw new IllegalStateException("User already refers to a UserSession; call User.removeCurrentUserSession() first");
| }
|
| aUserSession.setUser(this);
| getUserSessions().add(aUserSession);
|
| touch(); //force version update
| }
|
| /**
| * removes pointer to currentUserSession (if any). NOTE: this will not
| * result in UserSession object being deleted from DB.
| * session.delete(UserSession) needs to be executed to achieve the latter.
| *
| * @return removed currentServerSession
| */
| public UserSession removeCurrentUserSession() {
| final UserSession us = getCurrentUserSession();
| if (us != null && getUserSessions().remove(us)) {
| us.setUser(null);
| this.touch(); //force version update
| }
| return us;
| }
|
|
|
|
| }
| @Entity
| @org.hibernate.annotations.Entity(dynamicInsert = true, dynamicUpdate = true)
| @Table(name = "user_sessions")
| @Cache(usage = CacheConcurrencyStrategy.TRANSACTIONAL)
| public class UserSession extends AbstractUserSession {
|
| private User user;
|
| UserSession() {
| }
|
| public UserSession(User usr) {
| assert usr != null;
| setUser(usr);
| }
|
| @ManyToOne(fetch = FetchType.LAZY)
| @JoinColumn(unique = true, name = "userId")
| @NotNull
| @ForeignKey(name = "FK_USER_SESSION_TO_USER")
| public User getUser() {
| return user;
| }
|
| /**
| * @param user the user to set
| */
| void setUser(User user) {
| this.user = user;
| //touch();
| }
| }
|
|
Note that I tried putting synchronized (STATIC_LOCK) around init/getCurrent/removeUserSession in User and the test still failed. I suppose that means that the failure takes place around synchronization at TX commit time, not when domain objects are accessed (which makes sense).
Will keep digging further, but would appreciate any thoughts, of course.
View the original post : http://www.jboss.org/index.html?module=bb&op=viewtopic&p=4215463#4215463
Reply to the post : http://www.jboss.org/index.html?module=bb&op=posting&mode=reply&p=4215463
15 years, 10 months
[JBoss Cache: Core Edition] - Re: on child insert parent, child collection updated in DB b
by dukehoops
Ok, I was able to create a multithreaded JUnit test to fail (with just 4 threads).
A race condition seems to arise when multiple threads are trying to modify state of the same cache node(s). Here's the per-user test scenario:
public void run() {
| try {
| for (int i = 0; i < ITERATION_COUNT; i++) {
| login(userName);
| think();
|
| //get users
| getFriends2(userName);
| //readUserSession(userName);
| think();
|
| logout(userName);
| think();
|
| ++completedIterations;
| }
|
| } catch (Throwable t) {
| this.causeOfFailure = t;
| }
| }
If I omit getFriends2() -> test passes. Else, test fails.
If users participating in the test are NOT friends of each other (do not try to read each other's data) then test passes. Else, test fails.
getFriends() includes friends' login status. In other words User A will call User.getSessions() on user B if the 2 are friends.
Let's say user.userSessions#B is not in cache. Then A asks for B's info:
| A gets B's data:
| a1: userB.getSessions(); //will read from DB and put into cache
|
Simultaneously, B logs in:
B.login():
| b1: self.getSessions(); //will read from DB and put into cache
| b2: self.addSession(new UserSession()); //will invalidate user.userSessions#B
Seems like what's happening is that after both TXs are done, it is state from step 'a1' that ends up in cache.
Something like this order:
-a1 starts (cache miss, A is going to DB)
-b1
-b2
-a1 finished (A comes back from DB, inserts empty collection *after* B invalidated node in step b2)
Still not clear what the root cause is here. Some possibilities:
- my code
- hibernate core
- hibernate-jbc integration code
- jbc core
I guess the problem lies either with versioning (either Hibernate versioning or MVCC versioning) or (insufficient?) locking.
Side question on Synchronization ordering:
I looked at BTM source; seems like their Synchronization register is ordered:
| private List keys = new ArrayList();
| private Map objects = new TreeMap();
|
|
| public Scheduler() {
| }
|
| public void add(Object obj, int position) {
| Integer key = new Integer(position);
| List synchronizationsList = (List) objects.get(key);
| if (synchronizationsList == null) {
| if (!keys.contains(key)) {
| keys.add(key);
| Collections.sort(keys);
| }
| synchronizationsList = new ArrayList();
| objects.put(key, synchronizationsList);
| }
| synchronizationsList.add(obj);
| }
You said previously that assumptions around synchronization ordering were fixed for JBossTS and JBC specifically. Was the fix to order synchronizations in TX manager impl?
The reason I ask is that Spring seems to use a Synchronization to close Hibernate Sessions. Is there an expectation that *that* syncrhonization fire *before* JBC's synchronization?
thanks
-nikita
View the original post : http://www.jboss.org/index.html?module=bb&op=viewtopic&p=4215452#4215452
Reply to the post : http://www.jboss.org/index.html?module=bb&op=posting&mode=reply&p=4215452
15 years, 10 months
[JBoss jBPM] - designer bug: transition to superstate node not updated
by cvl
Hi.
I admit not doing any research, whereas this bug is found/solved/resolved, anyways:
in the designer (3.1.3.SP1), if you have superstate SS, node N in it, and transition from outside node pointing to SS/N, then if you rename superstate SS to SS1, this transition is not updated to SS1/N.
------
Additionally, in my view SuperState execute implementation is very weird one - taking the first node and entering it. When first means, the first in DB, which is usually the first in xml, that's very error prone in my view. Why not have something like start-node analog for it?
My use case is to enter this SuperState node on some process event, but now I need to add external Node to enter this SuperState. I cannot rely on sequence of xml entries of course.
--------
Also, putting start-state node into super-state (nonsense of course, but could be done accidentally), and then trying to delete it, throws classcast exception. Not nice.
2+1 cents
cvl
View the original post : http://www.jboss.org/index.html?module=bb&op=viewtopic&p=4215446#4215446
Reply to the post : http://www.jboss.org/index.html?module=bb&op=posting&mode=reply&p=4215446
15 years, 10 months