Change GameLock API (should be no behavior change)

This commit is contained in:
Eric House 2019-01-17 12:46:51 -08:00
parent f436090c6f
commit 026d6d5c5e
7 changed files with 230 additions and 175 deletions

View file

@ -2769,7 +2769,7 @@ public class BoardDelegate extends DelegateBase
summary = thread.getSummary(); summary = thread.getSummary();
gi = thread.getGI(); gi = thread.getGI();
} else { } else {
try ( GameLock lock = GameLock.getFor( rowID ).tryLockRO() ) { try ( GameLock lock = GameLock.tryLockRO( rowID ) ) {
if ( null != lock ) { if ( null != lock ) {
summary = DBUtils.getSummary( activity, lock ); summary = DBUtils.getSummary( activity, lock );
gi = new CurGameInfo( activity ); gi = new CurGameInfo( activity );

View file

@ -366,7 +366,7 @@ public class DBUtils {
public static void addRematchInfo( Context context, long rowid, String btAddr, public static void addRematchInfo( Context context, long rowid, String btAddr,
String phone, String relayID, String p2pAddr ) String phone, String relayID, String p2pAddr )
{ {
try ( GameLock lock = GameLock.getFor( rowid ).tryLock() ) { try ( GameLock lock = GameLock.tryLock(rowid) ) {
if ( null != lock ) { if ( null != lock ) {
GameSummary summary = getSummary( context, lock ); GameSummary summary = getSummary( context, lock );
if ( null != btAddr ) { if ( null != btAddr ) {
@ -1057,7 +1057,7 @@ public class DBUtils {
setCached( rowid, null ); // force reread setCached( rowid, null ); // force reread
lock = GameLock.getFor( rowid ).tryLock(); lock = GameLock.tryLock( rowid );
Assert.assertNotNull( lock ); Assert.assertNotNull( lock );
notifyListeners( rowid, GameChangeType.GAME_CREATED ); notifyListeners( rowid, GameChangeType.GAME_CREATED );
} }
@ -1119,7 +1119,7 @@ public class DBUtils {
public static void deleteGame( Context context, long rowid ) public static void deleteGame( Context context, long rowid )
{ {
try ( GameLock lock = GameLock.getFor( rowid ).lock( 300 ) ) { try ( GameLock lock = GameLock.lock( rowid, 300 ) ) {
if ( null != lock ) { if ( null != lock ) {
deleteGame( context, lock ); deleteGame( context, lock );
} else { } else {

View file

@ -663,7 +663,7 @@ public class DelegateBase implements DlgClickNotify,
public void onStatusClicked( long rowid ) public void onStatusClicked( long rowid )
{ {
Log.d( TAG, "onStatusClicked(%d)", rowid ); Log.d( TAG, "onStatusClicked(%d)", rowid );
try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) { try ( GameLock lock = GameLock.tryLockRO( rowid ) ) {
if ( null != lock ) { if ( null != lock ) {
GamePtr gamePtr = GameUtils.loadMakeGame( getActivity(), lock ); GamePtr gamePtr = GameUtils.loadMakeGame( getActivity(), lock );
if ( null != gamePtr ) { if ( null != gamePtr ) {

View file

@ -539,7 +539,7 @@ public class GameConfigDelegate extends DelegateBase
GameLock gameLock = null; GameLock gameLock = null;
XwJNI.GamePtr gamePtr = null; XwJNI.GamePtr gamePtr = null;
if ( null == m_jniThread ) { if ( null == m_jniThread ) {
gameLock = GameLock.getFor( m_rowid ).tryLockRO(); gameLock = GameLock.tryLockRO( m_rowid );
if ( null != gameLock ) { if ( null != gameLock ) {
gamePtr = GameUtils.loadMakeGame( m_activity, m_giOrig, gamePtr = GameUtils.loadMakeGame( m_activity, m_giOrig,
gameLock ); gameLock );
@ -1234,8 +1234,7 @@ public class GameConfigDelegate extends DelegateBase
{ {
if ( !isFinishing() ) { if ( !isFinishing() ) {
GameLock gameLock = m_jniThread == null GameLock gameLock = m_jniThread == null
? GameLock.getFor( m_rowid ).tryLock() ? GameLock.tryLock( m_rowid ) : m_jniThread.getLock();
: m_jniThread.getLock();
GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap, GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap,
gameLock, forceNew ); gameLock, forceNew );
DBUtils.saveThumbnail( m_activity, gameLock, null ); // clear it DBUtils.saveThumbnail( m_activity, gameLock, null ); // clear it

View file

@ -34,15 +34,28 @@ import android.support.annotation.NonNull;
// Implements read-locks and write-locks per game. A read lock is // Implements read-locks and write-locks per game. A read lock is
// obtainable when other read locks are granted but not when a // obtainable when other read locks are granted but not when a
// write lock is. Write-locks are exclusive. // write lock is. Write-locks are exclusive.
public class GameLock implements AutoCloseable { //
// Let's try representing a lock with something serializable. Let's not have
// one public object per game, but rather let lots of objects represent the
// same state. That way a lock can be grabbed by one thread or object (think
// GamesListDelegate) and held for as long as it takes the game that's opened
// to be closed. During that time it can be shared among objects
// (BoardDelegate and JNIThread, etc.) that know how to manage their
// interactions. (Note that I'm not doing this now -- found a way around it --
// but that the capability is still worth having, and so the change is going
// in. Having getFor() be public, and there being GameLock instances out there
// whose state was "unlocked", was just dumb.)
//
// So the class everybody sees (GameLock) is not stored. The rowid it
// holds is a key to a private Hash of state.
public class GameLock implements AutoCloseable, Serializable {
private static final String TAG = GameLock.class.getSimpleName(); private static final String TAG = GameLock.class.getSimpleName();
private static final boolean DEBUG_LOCKS = false; private static final boolean DEBUG_LOCKS = false;
// private static final long ASSERT_TIME = 2000; // private static final long ASSERT_TIME = 2000;
private static final long THROW_TIME = 1000; private static final long THROW_TIME = 1000;
private long m_rowid; private long m_rowid;
private Stack<Owner> mOwners = new Stack<>();
private boolean mReadOnly;
private static class Owner { private static class Owner {
Thread mThread; Thread mThread;
@ -62,26 +75,195 @@ public class GameLock implements AutoCloseable {
} }
} }
@Override private static class GameLockState {
public String toString() long mRowid;
{ private Stack<Owner> mOwners = new Stack<>();
return String.format("{this: %H; rowid: %d; count: %d; ro: %b}", private boolean mReadOnly;
this, m_rowid, mOwners.size(), mReadOnly);
GameLockState( long rowid ) { mRowid = rowid; }
// We grant a lock IFF:
// * Count is 0
// OR
// * existing locks are ReadOnly and this request is readOnly
// OR
// * the requesting thread already holds the lock (later...)
// // This could be written to allow multiple read locks. Let's
// // see if not doing that causes problems.
private GameLock tryLockImpl( boolean readOnly )
{
GameLock result = null;
synchronized ( mOwners ) {
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.tryLockImpl(ro=%b)", this, readOnly );
}
// Thread thisThread = Thread.currentThread();
boolean grant = false;
if ( mOwners.empty() ) {
grant = true;
} else if ( mReadOnly && readOnly ) {
grant = true;
// } else if ( thisThread == mOwnerThread ) {
// grant = true;
}
if ( grant ) {
mOwners.push( new Owner() );
mReadOnly = readOnly;
result = new GameLock( mRowid );
}
}
return result;
}
private GameLock tryLockRO()
{
GameLock result = tryLockImpl( true );
logIfNull( result, "tryLockRO()" );
return result;
}
private GameLock lockImpl( long timeoutMS, boolean readOnly ) throws InterruptedException
{
long startMS = System.currentTimeMillis();
long endMS = startMS + timeoutMS;
synchronized ( mOwners ) {
while ( null == tryLockImpl( readOnly ) ) {
long now = System.currentTimeMillis();
if ( now >= endMS ) {
throw new GameLockedException();
}
mOwners.wait( endMS - now );
}
}
if ( DEBUG_LOCKS ) {
long tookMS = System.currentTimeMillis() - startMS;
Log.d( TAG, "%s.lockImpl() returning after %d ms", this, tookMS );
}
return new GameLock(mRowid);
}
// Version that's allowed to return null -- if maxMillis > 0
private GameLock lock( long maxMillis ) throws GameLockedException
{
Assert.assertTrue( maxMillis <= THROW_TIME );
GameLock result = null;
try {
result = lockImpl( maxMillis, false );
} catch (InterruptedException ex) {
Log.d( TAG, "lock(): got %s", ex.getMessage() );
}
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.lock(%d) => %s", this, maxMillis, result );
}
logIfNull( result, "lock(maxMillis=%d)", maxMillis );
return result;
}
private GameLock tryLock()
{
GameLock result = tryLockImpl( false );
logIfNull( result, "tryLock()" );
return result;
}
public GameLock lock() throws InterruptedException
{
if ( BuildConfig.DEBUG ) {
DbgUtils.assertOnUIThread( false );
}
GameLock result = lockImpl( Long.MAX_VALUE, false );
Assert.assertNotNull( result );
return result;
}
private GameLock lockRO( long maxMillis )
{
Assert.assertTrue( maxMillis <= THROW_TIME );
GameLock lock = null;
try {
lock = lockImpl( maxMillis, true );
} catch ( InterruptedException ex ) {
}
logIfNull( lock, "lockRO(maxMillis=%d)", maxMillis );
return lock;
}
private void unlock()
{
synchronized ( mOwners ) {
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.unlock()", this );
}
Thread oldThread = mOwners.pop().mThread;
// It's ok for different threads to hold the same RO lock
if ( !mReadOnly && oldThread != Thread.currentThread() ) {
Log.e( TAG, "unlock(): unequal threads: %s => %s", oldThread,
Thread.currentThread() );
Assert.fail();
}
if ( mOwners.empty() ) {
mOwners.notifyAll();
}
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.unlock() DONE", this );
}
}
}
private boolean canWrite()
{
boolean result = !mReadOnly; // && 1 == mLockCount[0];
if ( !result ) {
Log.w( TAG, "%s.canWrite(): => false", this );
}
return result;
}
private void setOwner( Owner owner )
{
synchronized ( mOwners ) {
mOwners.pop();
mOwners.push( owner );
}
}
@Override
public String toString()
{
return String.format("{this: %H; rowid: %d; count: %d; ro: %b}",
this, mRowid, mOwners.size(), mReadOnly);
}
private void logIfNull( GameLock result, String fmt, Object... args )
{
if ( DEBUG_LOCKS && null == result ) {
String func = new Formatter().format( fmt, args ).toString();
Log.d( TAG, "%s.%s => null", this, func );
Log.d( TAG, "Unable to lock; cur owner: %s; would-be owner: %s",
mOwners.peek(), new Owner() );
}
}
} }
public static class GameLockedException extends RuntimeException {} public static class GameLockedException extends RuntimeException {}
private static Map<Long, WeakReference<GameLock>> sLockMap = new HashMap<>(); private static Map<Long, GameLockState> sLockMap = new HashMap<>();
public static GameLock getFor( long rowid ) private static GameLockState getFor( long rowid )
{ {
GameLock result = null; GameLockState result = null;
synchronized ( sLockMap ) { synchronized ( sLockMap ) {
if ( sLockMap.containsKey( rowid ) ) { if ( sLockMap.containsKey( rowid ) ) {
result = sLockMap.get( rowid ).get(); result = sLockMap.get( rowid );
} }
if ( null == result ) { if ( null == result ) {
result = new GameLock( rowid ); result = new GameLockState( rowid );
sLockMap.put( rowid, new WeakReference(result) ); sLockMap.put( rowid, result );
} }
} }
return result; return result;
@ -89,138 +271,35 @@ public class GameLock implements AutoCloseable {
private GameLock( long rowid ) { m_rowid = rowid; } private GameLock( long rowid ) { m_rowid = rowid; }
// We grant a lock IFF: public static GameLock tryLock( long rowid )
// * Count is 0
// OR
// * existing locks are ReadOnly and this request is readOnly
// OR
// * the requesting thread already holds the lock (later...)
private GameLock tryLockImpl( boolean readOnly )
{ {
GameLock result = null; return getFor( rowid ).tryLock();
synchronized ( mOwners ) {
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.tryLockImpl(ro=%b)", this, readOnly );
}
// Thread thisThread = Thread.currentThread();
boolean grant = false;
if ( mOwners.empty() ) {
grant = true;
} else if ( mReadOnly && readOnly ) {
grant = true;
// } else if ( thisThread == mOwnerThread ) {
// grant = true;
}
if ( grant ) {
mOwners.push( new Owner() );
mReadOnly = readOnly;
result = this;
}
}
return result;
}
// // This could be written to allow multiple read locks. Let's
// // see if not doing that causes problems.
public GameLock tryLock()
{
GameLock result = tryLockImpl( false );
logIfNull( result, "tryLock()" );
return result;
} }
public GameLock tryLockRO() public static GameLock tryLockRO(long rowid)
{ {
GameLock result = tryLockImpl( true ); return getFor( rowid ).tryLockRO();
logIfNull( result, "tryLockRO()" );
return result;
}
private GameLock lockImpl( long timeoutMS, boolean readOnly ) throws InterruptedException
{
long startMS = System.currentTimeMillis();
long endMS = startMS + timeoutMS;
synchronized ( mOwners ) {
while ( null == tryLockImpl( readOnly ) ) {
long now = System.currentTimeMillis();
if ( now >= endMS ) {
throw new GameLockedException();
}
mOwners.wait( endMS - now );
}
}
if ( DEBUG_LOCKS ) {
long tookMS = System.currentTimeMillis() - startMS;
Log.d( TAG, "%s.lockImpl() returning after %d ms", this, tookMS );
}
return this;
} }
@NonNull @NonNull
public GameLock lock() throws InterruptedException public static GameLock lock(long rowid) throws InterruptedException
{ {
if ( BuildConfig.DEBUG ) { return getFor( rowid ).lock();
DbgUtils.assertOnUIThread( false );
}
GameLock result = lockImpl( Long.MAX_VALUE, false );
Assert.assertNotNull( result );
return result;
} }
// Version that's allowed to return null -- if maxMillis > 0 public static GameLock lock( long rowid, long maxMillis ) throws GameLockedException
public GameLock lock( long maxMillis ) throws GameLockedException
{ {
Assert.assertTrue( maxMillis <= THROW_TIME ); return getFor( rowid ).lock( maxMillis );
GameLock result = null;
try {
result = lockImpl( maxMillis, false );
} catch (InterruptedException ex) {
Log.d( TAG, "lock(): got %s", ex.getMessage() );
}
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.lock(%d) => %s", this, maxMillis, result );
}
logIfNull( result, "lock(maxMillis=%d)", maxMillis );
return result;
} }
public GameLock lockRO( long maxMillis ) public static GameLock lockRO( long rowid, long maxMillis )
{ {
Assert.assertTrue( maxMillis <= THROW_TIME ); return getFor( rowid ).lockRO( maxMillis );
GameLock lock = null;
try {
lock = lockImpl( maxMillis, true );
} catch ( InterruptedException ex ) {
}
logIfNull( lock, "lockRO(maxMillis=%d)", maxMillis );
return lock;
} }
public void unlock() public void unlock()
{ {
synchronized ( mOwners ) { getFor( m_rowid ).unlock();
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.unlock()", this );
}
Thread oldThread = mOwners.pop().mThread;
// It's ok for different threads to hold the same RO lock
if ( !mReadOnly && oldThread != Thread.currentThread() ) {
Log.e( TAG, "unlock(): unequal threads: %s => %s", oldThread,
Thread.currentThread() );
Assert.fail();
}
if ( mOwners.empty() ) {
mOwners.notifyAll();
}
if ( DEBUG_LOCKS ) {
Log.d( TAG, "%s.unlock() DONE", this );
}
}
} }
@Override @Override
@ -234,14 +313,6 @@ public class GameLock implements AutoCloseable {
return m_rowid; return m_rowid;
} }
private void setOwner( Owner owner )
{
synchronized ( mOwners ) {
mOwners.pop();
mOwners.push( owner );
}
}
public interface LockProc { public interface LockProc {
public void gotLock( GameLock lock ); public void gotLock( GameLock lock );
} }
@ -268,10 +339,9 @@ public class GameLock implements AutoCloseable {
} catch ( Exception ex) {} } catch ( Exception ex) {}
} else { } else {
try { try {
lock = GameLock GameLockState state = getFor( rowid );
.getFor( rowid ) lock = state.lockImpl( maxMillis, false );
.lockImpl( maxMillis, false ); state.setOwner( owner );
lock.setOwner( owner );
} catch ( GameLockedException | InterruptedException gle ) {} } catch ( GameLockedException | InterruptedException gle ) {}
} }
@ -289,20 +359,6 @@ public class GameLock implements AutoCloseable {
// used only for asserts // used only for asserts
public boolean canWrite() public boolean canWrite()
{ {
boolean result = !mReadOnly; // && 1 == mLockCount[0]; return getFor( m_rowid ).canWrite();
if ( !result ) {
Log.w( TAG, "%s.canWrite(): => false", this );
}
return result;
}
private void logIfNull( GameLock result, String fmt, Object... args )
{
if ( DEBUG_LOCKS && null == result ) {
String func = new Formatter().format( fmt, args ).toString();
Log.d( TAG, "%s.%s => null", this, func );
Log.d( TAG, "Unable to lock; cur owner: %s; would-be owner: %s",
mOwners.peek(), new Owner() );
}
} }
} }

View file

@ -93,7 +93,7 @@ public class GameUtils {
public static byte[] savedGame( Context context, long rowid ) public static byte[] savedGame( Context context, long rowid )
{ {
byte[] result = null; byte[] result = null;
try (GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) { try (GameLock lock = GameLock.tryLockRO( rowid ) ) {
if ( null != lock ) { if ( null != lock ) {
result = savedGame( context, lock ); result = savedGame( context, lock );
} }
@ -147,7 +147,7 @@ public class GameUtils {
if ( null == lockDest ) { if ( null == lockDest ) {
long groupID = DBUtils.getGroupForGame( context, lockSrc.getRowid() ); long groupID = DBUtils.getGroupForGame( context, lockSrc.getRowid() );
long rowid = saveNewGame( context, gamePtr, gi, groupID ); long rowid = saveNewGame( context, gamePtr, gi, groupID );
lockDest = GameLock.getFor( rowid ).tryLock(); lockDest = GameLock.tryLock( rowid );
} else { } else {
saveGame( context, gamePtr, gi, lockDest, true ); saveGame( context, gamePtr, gi, lockDest, true );
} }
@ -160,7 +160,7 @@ public class GameUtils {
public static boolean resetGame( Context context, long rowidIn ) public static boolean resetGame( Context context, long rowidIn )
{ {
boolean success = false; boolean success = false;
try ( GameLock lock = GameLock.getFor( rowidIn ).lock( 500 ) ) { try ( GameLock lock = GameLock.lock( rowidIn, 500 ) ) {
if ( null != lock ) { if ( null != lock ) {
tellDied( context, lock, true ); tellDied( context, lock, true );
resetGame( context, lock, lock, false ); resetGame( context, lock, lock, false );
@ -225,7 +225,7 @@ public class GameUtils {
lock = thread.getLock(); lock = thread.getLock();
} else { } else {
try { try {
lock = GameLock.getFor( rowid ).lockRO( maxMillis ); lock = GameLock.lockRO( rowid, maxMillis );
} catch ( GameLock.GameLockedException gle ) { } catch ( GameLock.GameLockedException gle ) {
Log.ex( TAG, gle ); Log.ex( TAG, gle );
} }
@ -256,7 +256,7 @@ public class GameUtils {
if ( null != thread ) { if ( null != thread ) {
lockSrc = thread.getLock(); lockSrc = thread.getLock();
} else { } else {
lockSrc = GameLock.getFor( rowidIn ).lockRO( 300 ); lockSrc = GameLock.lockRO( rowidIn, 300 );
} }
if ( null != lockSrc ) { if ( null != lockSrc ) {
@ -292,7 +292,7 @@ public class GameUtils {
{ {
boolean success; boolean success;
// does this need to be synchronized? // does this need to be synchronized?
try ( GameLock lock = GameLock.getFor( rowid ).tryLock() ) { try ( GameLock lock = GameLock.tryLock( rowid ) ) {
if ( null != lock ) { if ( null != lock ) {
deleteGame( context, lock, informNow ); deleteGame( context, lock, informNow );
success = true; success = true;
@ -397,7 +397,7 @@ public class GameUtils {
public static Bitmap loadMakeBitmap( Context context, long rowid ) public static Bitmap loadMakeBitmap( Context context, long rowid )
{ {
Bitmap thumb = null; Bitmap thumb = null;
try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) { try ( GameLock lock = GameLock.tryLockRO( rowid ) ) {
if ( null != lock ) { if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( context ); CurGameInfo gi = new CurGameInfo( context );
GamePtr gamePtr = loadMakeGame( context, gi, lock ); GamePtr gamePtr = loadMakeGame( context, gi, lock );
@ -649,7 +649,7 @@ public class GameUtils {
if ( DBUtils.ROWID_NOTFOUND != rowid ) { if ( DBUtils.ROWID_NOTFOUND != rowid ) {
// Use tryLock in case we're on UI thread. It's guaranteed to // Use tryLock in case we're on UI thread. It's guaranteed to
// succeed because we just created the rowid. // succeed because we just created the rowid.
try ( GameLock lock = GameLock.getFor( rowid ).tryLock() ) { try ( GameLock lock = GameLock.tryLock( rowid ) ) {
Assert.assertNotNull( lock ); Assert.assertNotNull( lock );
applyChanges( context, sink, gi, util, addr, null, lock, false ); applyChanges( context, sink, gi, util, addr, null, lock, false );
} }
@ -910,7 +910,7 @@ public class GameUtils {
// have the lock and we'll never get it. Better to drop // have the lock and we'll never get it. Better to drop
// the message than fire the hung-lock assert. Messages // the message than fire the hung-lock assert. Messages
// belong in local pre-delivery storage anyway. // belong in local pre-delivery storage anyway.
try ( GameLock lock = GameLock.getFor( rowid ).lock( 150 ) ) { try ( GameLock lock = GameLock.lock( rowid, 150 ) ) {
if ( null != lock ) { if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( context ); CurGameInfo gi = new CurGameInfo( context );
FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid ); FeedUtilsImpl feedImpl = new FeedUtilsImpl( context, rowid );
@ -973,7 +973,7 @@ public class GameUtils {
String oldDict, String newDict ) String oldDict, String newDict )
{ {
boolean success; boolean success;
try ( GameLock lock = GameLock.getFor( rowid ).lock(300) ) { try ( GameLock lock = GameLock.lock( rowid, 300 ) ) {
success = null != lock; success = null != lock;
if ( success ) { if ( success ) {
byte[] stream = savedGame( context, lock ); byte[] stream = savedGame( context, lock );
@ -1258,7 +1258,7 @@ public class GameUtils {
} }
} }
try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) { try ( GameLock lock = GameLock.tryLockRO( rowid ) ) {
if ( null != lock ) { if ( null != lock ) {
CurGameInfo gi = new CurGameInfo( m_context ); CurGameInfo gi = new CurGameInfo( m_context );
MultiMsgSink sink = new MultiMsgSink( m_context, rowid ); MultiMsgSink sink = new MultiMsgSink( m_context, rowid );

View file

@ -134,7 +134,7 @@ abstract class XWServiceHelper {
for ( int ii = 0; success && ii < rowids.length; ++ii ) { for ( int ii = 0; success && ii < rowids.length; ++ii ) {
long rowid = rowids[ii]; long rowid = rowids[ii];
CurGameInfo gi = null; CurGameInfo gi = null;
try ( GameLock lock = GameLock.getFor( rowid ).tryLockRO() ) { try ( GameLock lock = GameLock.tryLockRO( rowid ) ) {
// drop invite if can't open game; likely a dupe! // drop invite if can't open game; likely a dupe!
if ( null != lock ) { if ( null != lock ) {
gi = new CurGameInfo( mService ); gi = new CurGameInfo( mService );