Make JNIThread autoclosable and use everywhere

JNIThread is somehow sticking around sometimes and holding the lock for
a game so that that game can never be opened again. On the theory that
there's some place retain() was called but not release(), use the thing
in try-with-resources wherever possible. Which is pretty much
everywhere.

Also added age to the lock-holder report being uploaded.
This commit is contained in:
Eric House 2019-02-11 20:39:17 -08:00
parent 9fe01047b1
commit b1a4b1a030
11 changed files with 184 additions and 152 deletions

View file

@ -89,7 +89,6 @@ public class BoardDelegate extends DelegateBase
private Activity m_activity;
private BoardView m_view;
private GamePtr m_jniGamePtr;
private GameLock m_gameLock;
private CurGameInfo m_gi;
private GameSummary m_summary;
private boolean m_relayMissing;
@ -539,16 +538,16 @@ public class BoardDelegate extends DelegateBase
m_haveInvited = args.getBoolean( GameUtils.INVITED, false );
m_overNotShown = true;
GameLock.callWithLock( m_rowid, 100L, new Handler(),
new GameLock.LockProc() {
GameLock.getLockThen( m_rowid, 100L, new Handler(),
new GameLock.GotLockProc() {
@Override
public void gotLock( GameLock lock ) {
if ( null == lock ) {
finish();
if ( BuildConfig.REPORT_LOCKS && ++s_noLockCount == 3 ) {
String msg = "BoardDelegate unable to get lock; holder stack: "
+ GameLock.getHolderStack( m_rowid );
CrashTrack.logAndSend( msg );
+ GameLock.getHolderDump( m_rowid );
CrashTrack.logAndSend( TAG, msg );
}
} else {
s_noLockCount = 0;
@ -1501,7 +1500,7 @@ public class BoardDelegate extends DelegateBase
private void deleteAndClose()
{
GameUtils.deleteGame( m_activity, m_gameLock, false );
GameUtils.deleteGame( m_activity, m_jniThread.getLock(), false );
waitCloseGame( false );
finish();
}
@ -2158,7 +2157,6 @@ public class BoardDelegate extends DelegateBase
m_jniThread = m_jniThreadRef.retain();
m_gi = m_jniThread.getGI();
m_summary = m_jniThread.getSummary();
m_gameLock = m_jniThread.getLock();
m_view.startHandling( m_activity, m_jniThread, m_connTypes );
@ -2427,8 +2425,6 @@ public class BoardDelegate extends DelegateBase
m_jniThread = null;
m_view.stopHandling();
m_gameLock = null;
}
}
@ -2440,7 +2436,6 @@ public class BoardDelegate extends DelegateBase
// m_jniGamePtr = null;
// m_gameLock.unlock(); // likely the problem
m_gameLock = null;
}
}
@ -2793,34 +2788,32 @@ public class BoardDelegate extends DelegateBase
GamePtr gamePtr = null;
GameSummary summary = null;
CurGameInfo gi = null;
JNIThread thread = JNIThread.getRetained( rowID );
if ( null != thread ) {
gamePtr = thread.getGamePtr().retain();
summary = thread.getSummary();
gi = thread.getGI();
} else {
try ( GameLock lock = GameLock.tryLockRO( rowID ) ) {
if ( null != lock ) {
summary = DBUtils.getSummary( activity, lock );
gi = new CurGameInfo( activity );
gamePtr = GameUtils.loadMakeGame( activity, gi, lock );
} else {
DbgUtils.toastNoLock( TAG, activity, rowID,
"setupRematchFor(%d)", rowID );
try ( JNIThread thread = JNIThread.getRetained( rowID ) ) {
if ( null != thread ) {
gamePtr = thread.getGamePtr().retain();
summary = thread.getSummary();
gi = thread.getGI();
} else {
try ( GameLock lock = GameLock.tryLockRO( rowID ) ) {
if ( null != lock ) {
summary = DBUtils.getSummary( activity, lock );
gi = new CurGameInfo( activity );
gamePtr = GameUtils.loadMakeGame( activity, gi, lock );
} else {
DbgUtils.toastNoLock( TAG, activity, rowID,
"setupRematchFor(%d)", rowID );
}
}
}
}
if ( null != gamePtr ) {
doRematchIf( activity, null, rowID, DBUtils.GROUPID_UNSPEC,
summary, gi, gamePtr );
gamePtr.release();
} else {
Log.w( TAG, "setupRematchFor(): unable to lock game" );
}
if ( null != thread ) {
thread.release();
if ( null != gamePtr ) {
doRematchIf( activity, null, rowID, DBUtils.GROUPID_UNSPEC,
summary, gi, gamePtr );
gamePtr.release();
} else {
Log.w( TAG, "setupRematchFor(): unable to lock game" );
}
}
}

View file

@ -59,7 +59,6 @@ public class ChatDelegate extends DelegateBase {
private EditText m_edit;
private TableLayout m_layout;
private ScrollView m_scroll;
private JNIThread m_jniThreadRef;
public ChatDelegate( Delegator delegator, Bundle savedInstanceState )
{
@ -130,28 +129,20 @@ public class ChatDelegate extends DelegateBase {
protected void onResume()
{
super.onResume();
m_jniThreadRef = JNIThread.getRetained( m_rowid );
if ( null == m_jniThreadRef ) {
Log.w( TAG, "onResume(): m_jniThreadRef null; exiting" );
finish();
} else {
s_visibleThis = this;
int[] startAndEnd = new int[2];
String curMsg = DBUtils.getCurChat( m_activity, m_rowid,
m_curPlayer, startAndEnd );
if ( null != curMsg && 0 < curMsg.length() ) {
m_edit.setText( curMsg );
m_edit.setSelection( startAndEnd[0], startAndEnd[1] );
}
s_visibleThis = this;
int[] startAndEnd = new int[2];
String curMsg = DBUtils.getCurChat( m_activity, m_rowid,
m_curPlayer, startAndEnd );
if ( null != curMsg && 0 < curMsg.length() ) {
m_edit.setText( curMsg );
m_edit.setSelection( startAndEnd[0], startAndEnd[1] );
}
}
@Override
protected void onPause()
{
if ( null != m_jniThreadRef ) {
m_jniThreadRef.release();
}
s_visibleThis = null;
String curText = m_edit.getText().toString();
@ -205,7 +196,11 @@ public class ChatDelegate extends DelegateBase {
addRow( text, m_curPlayer, (int)ts );
m_edit.setText( null );
m_jniThreadRef.sendChat( text );
try ( JNIThread thread = JNIThread.getRetained( m_rowid ) ) {
if ( null != thread ) {
thread.sendChat( text );
}
}
}
@Override

View file

@ -66,7 +66,7 @@ public class DbgUtils {
}
Log.w( tag, format, args );
Log.w( tag, "stack for lock owner for %d", rowid );
Log.w( tag, GameLock.getHolderStack( rowid ) );
Log.w( tag, GameLock.getHolderDump( rowid ) );
}
public static void assertOnUIThread()

View file

@ -536,17 +536,16 @@ public class GameConfigDelegate extends DelegateBase
if ( null == m_giOrig ) {
m_giOrig = new CurGameInfo( m_activity );
GameLock gameLock = null;
XwJNI.GamePtr gamePtr = null;
if ( null == m_jniThread ) {
gameLock = GameLock.tryLockRO( m_rowid );
if ( null != gameLock ) {
gamePtr = GameUtils.loadMakeGame( m_activity, m_giOrig,
gameLock );
}
if ( null != m_jniThread ) {
gamePtr = m_jniThread.getGamePtr().retain();
} else {
gameLock = m_jniThread.getLock();
gamePtr = m_jniThread.getGamePtr();
try ( GameLock lock = GameLock.tryLockRO( m_rowid ) ) {
if ( null != lock ) {
gamePtr = GameUtils.loadMakeGame( m_activity, m_giOrig,
lock );
}
}
}
if ( null == gamePtr ) {
@ -588,10 +587,7 @@ public class GameConfigDelegate extends DelegateBase
buildDisabledsMap( gamePtr );
setDisableds();
if ( null == m_jniThread ) {
gamePtr.release();
gameLock.unlock();
}
gamePtr.release();
m_car = new CommsAddrRec( m_carOrig );
@ -1230,17 +1226,32 @@ public class GameConfigDelegate extends DelegateBase
m_car.conTypes = m_conTypes;
} // saveChanges
private void applyChanges( GameLock lock, boolean forceNew )
{
GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap,
lock, forceNew );
DBUtils.saveThumbnail( m_activity, lock, null ); // clear it
}
private void applyChanges( boolean forceNew )
{
if ( !isFinishing() ) {
GameLock gameLock = m_jniThread == null
? GameLock.tryLock( m_rowid ) : m_jniThread.getLock();
GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap,
gameLock, forceNew );
DBUtils.saveThumbnail( m_activity, gameLock, null ); // clear it
if ( null == m_jniThread ) {
gameLock.unlock();
if ( null != m_jniThread ) {
applyChanges( m_jniThread.getLock(), forceNew );
} else {
try ( GameLock lock = GameLock.tryLock( m_rowid ) ) {
applyChanges( lock, forceNew );
}
}
// }
// GameLock gameLock = m_jniThread == null
// ? GameLock.tryLock( m_rowid ) : m_jniThread.getLock();
// GameUtils.applyChanges( m_activity, m_gi, m_car, m_disabMap,
// gameLock, forceNew );
// DBUtils.saveThumbnail( m_activity, gameLock, null ); // clear it
// if ( null == m_jniThread ) {
// gameLock.unlock();
// }
}
}

View file

@ -60,18 +60,24 @@ public class GameLock implements AutoCloseable, Serializable {
private static class Owner {
Thread mThread;
String mTrace;
long mStamp;
Owner()
{
mThread = Thread.currentThread();
mTrace = android.util.Log.getStackTraceString(new Exception());
setStamp();
}
@Override
public String toString()
{
return String.format( "Owner: {%s/%s}", mThread, mTrace );
long ageMS = System.currentTimeMillis() - mStamp;
return String.format( "Owner: {age: %dms; thread: {%s}; stack: {%s}}",
ageMS, mThread, mTrace );
}
void setStamp() { mStamp = System.currentTimeMillis(); }
}
private static class GameLockState {
@ -233,6 +239,7 @@ public class GameLock implements AutoCloseable, Serializable {
{
synchronized ( mOwners ) {
mOwners.pop();
owner.setStamp();
mOwners.push( owner );
}
}
@ -296,7 +303,7 @@ public class GameLock implements AutoCloseable, Serializable {
return getFor( rowid ).lock( maxMillis );
}
public static GameLock lockRO( long rowid, long maxMillis )
public static GameLock lockRO( long rowid, long maxMillis ) throws GameLockedException
{
return getFor( rowid ).lockRO( maxMillis );
}
@ -317,17 +324,17 @@ public class GameLock implements AutoCloseable, Serializable {
return m_rowid;
}
public interface LockProc {
public interface GotLockProc {
public void gotLock( GameLock lock );
}
// Meant to be called from UI thread, returning immediately, but when it
// gets the lock, or time runs out, calls the callback (using the Handler
// passed in) with the lock or null.
public static void callWithLock( final long rowid,
final long maxMillis,
final Handler handler,
final LockProc proc )
public static void getLockThen( final long rowid,
final long maxMillis,
final Handler handler,
final GotLockProc proc )
{
// capture caller thread and stack
final Owner owner = new Owner();
@ -360,11 +367,11 @@ public class GameLock implements AutoCloseable, Serializable {
} ).start();
}
public static String getHolderStack( long rowid )
public static String getHolderDump( long rowid )
{
GameLockState state = getFor( rowid );
Owner owner = state.mOwners.peek();
return owner.mTrace;
return owner.toString();
}
// used only for asserts

View file

@ -225,24 +225,16 @@ public class GameUtils {
long maxMillis )
{
GameSummary result = null;
JNIThread thread = JNIThread.getRetained( rowid );
GameLock lock = null;
if ( null != thread ) {
lock = thread.getLock();
} else {
try {
lock = GameLock.lockRO( rowid, maxMillis );
} catch ( GameLock.GameLockedException gle ) {
Log.ex( TAG, gle );
}
}
if ( null != lock ) {
result = DBUtils.getSummary( context, lock );
if ( null == thread ) {
lock.unlock();
try ( JNIThread thread = JNIThread.getRetained( rowid ) ) {
if ( null != thread ) {
result = DBUtils.getSummary( context, thread.getLock() );
} else {
thread.release();
try ( GameLock lock = GameLock.lockRO( rowid, maxMillis ) ) {
if ( null != lock ) {
result = DBUtils.getSummary( context, lock );
}
} catch ( GameLock.GameLockedException gle ) {
}
}
}
return result;
@ -260,35 +252,41 @@ public class GameUtils {
public static long dupeGame( Context context, long rowidIn, long groupID )
{
long rowid = DBUtils.ROWID_NOTFOUND;
GameLock lockSrc = null;
long result = DBUtils.ROWID_NOTFOUND;
JNIThread thread = JNIThread.getRetained( rowidIn );
if ( null != thread ) {
lockSrc = thread.getLock();
} else {
lockSrc = GameLock.lockRO( rowidIn, 300 );
try ( JNIThread thread = JNIThread.getRetained( rowidIn ) ) {
if ( null != thread ) {
result = dupeGame( context, thread.getLock(), groupID );
} else {
try ( GameLock lockSrc = GameLock.lockRO( rowidIn, 300 ) ) {
if ( null != lockSrc ) {
result = dupeGame( context, lockSrc, groupID );
}
} catch ( GameLock.GameLockedException gle ) {
}
}
}
if ( null != lockSrc ) {
boolean juggle = CommonPrefs.getAutoJuggle( context );
GameLock lockDest = resetGame( context, lockSrc, null, groupID,
juggle );
rowid = lockDest.getRowid();
lockDest.unlock();
if ( null != thread ) {
thread.release();
} else {
lockSrc.unlock();
}
} else {
if ( DBUtils.ROWID_NOTFOUND == result ) {
Log.d( TAG, "dupeGame: unable to open rowid %d", rowidIn );
}
return rowid;
return result;
}
public static void deleteGame( Context context, GameLock lock, boolean informNow )
private static long dupeGame( Context context, GameLock lock, long groupID )
{
long result;
boolean juggle = CommonPrefs.getAutoJuggle( context );
try ( GameLock lockDest = resetGame( context, lock,
null, groupID,
juggle ) ) {
result = lockDest.getRowid();
}
return result;
}
public static void deleteGame( Context context, GameLock lock,
boolean informNow )
{
if ( null != lock ) {
tellDied( context, lock, informNow );
@ -1290,14 +1288,14 @@ public class GameUtils {
+ " failed for rowid %d", rowid );
}
} else {
JNIThread jniThread = JNIThread.getRetained( rowid );
if ( null != jniThread ) {
jniThread.handle( JNIThread.JNICmd.CMD_RESEND, false,
false, false );
jniThread.release();
} else {
Log.w( TAG, "Resender.doInBackground: unable to unlock %d",
rowid );
try ( JNIThread thread = JNIThread.getRetained( rowid ) ) {
if ( null != thread ) {
thread.handle( JNIThread.JNICmd.CMD_RESEND, false,
false, false );
} else {
Log.w( TAG, "Resender.doInBackground: unable to unlock %d",
rowid );
}
}
}
}

View file

@ -76,21 +76,23 @@ abstract class XWServiceHelper {
{
boolean allConsumed = true;
boolean[] isLocalP = new boolean[1];
JNIThread jniThread = JNIThread.getRetained( rowid );
boolean consumed = false;
if ( null != jniThread ) {
consumed = true;
jniThread.receive( msg, addr ).release();
} else {
GameUtils.BackMoveResult bmr = new GameUtils.BackMoveResult();
if ( null == sink ) {
sink = getSink( rowid );
}
if ( GameUtils.feedMessage( context, rowid, msg, addr,
sink, bmr, isLocalP ) ) {
try ( JNIThread jniThread = JNIThread.getRetained( rowid ) ) {
if ( null != jniThread ) {
jniThread.receive( msg, addr );
consumed = true;
GameUtils.postMoveNotification( context, rowid, bmr,
isLocalP[0] );
} else {
GameUtils.BackMoveResult bmr = new GameUtils.BackMoveResult();
if ( null == sink ) {
sink = getSink( rowid );
}
if ( GameUtils.feedMessage( context, rowid, msg, addr,
sink, bmr, isLocalP ) ) {
GameUtils.postMoveNotification( context, rowid, bmr,
isLocalP[0] );
consumed = true;
}
}
}
if ( allConsumed && !consumed ) {
@ -149,10 +151,10 @@ abstract class XWServiceHelper {
if ( null == gi ) {
// locked. Maybe it's open?
JNIThread thread = JNIThread.getRetained( rowid );
if ( null != thread ) {
gi = thread.getGI();
thread.release( false );
try ( JNIThread thrd = JNIThread.getRetained( rowid ) ) {
if ( null != thrd ) {
gi = thrd.getGI();
}
}
}
success = null != gi && gi.forceChannel != nli.forceChannel;

View file

@ -49,7 +49,7 @@ import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
public class JNIThread extends Thread {
public class JNIThread extends Thread implements AutoCloseable {
private static final String TAG = JNIThread.class.getSimpleName();
public enum JNICmd { CMD_NONE,
@ -265,7 +265,16 @@ public class JNIThread extends Thread {
} catch ( java.lang.InterruptedException ie ) {
Log.ex( TAG, ie );
}
m_lock.unlock();
unlockOnce();
}
private synchronized void unlockOnce()
{
if ( null != m_lock ) {
m_lock.unlock();
m_lock = null;
}
}
public boolean busy()
@ -746,9 +755,18 @@ public class JNIThread extends Thread {
m_jniGamePtr.release();
m_jniGamePtr = null;
}
unlockOnce();
Log.d( TAG, "run() finished" );
} // run
@Override
public void finalize() throws java.lang.Throwable
{
Assert.assertTrue( null == m_lock || !BuildConfig.DEBUG );
super.finalize();
}
public void handleBkgrnd( JNICmd cmd, Object... args )
{
// DbgUtils.logf( "adding: %s", cmd.toString() );
@ -823,6 +841,12 @@ public class JNIThread extends Thread {
}
}
@Override
public void close()
{
release();
}
public static JNIThread getRetained( long rowid )
{
return getRetained( rowid, null );

View file

@ -467,7 +467,6 @@ public class XwJNI {
release();
super.finalize();
}
}
public static native boolean dict_tilesAreSame( int dict1, int dict2 );

View file

@ -24,5 +24,5 @@ import android.content.Context;
public class CrashTrack {
public static void init( Context context ) {} // does nothing here
public static void logAndSend( String msg ) {}
public static void logAndSend( String tag, String msg ) {}
}

View file

@ -61,9 +61,12 @@ public class CrashTrack {
}
}
public static void logAndSend( String msg )
public static void logAndSend( String tag, String msg )
{
Crashlytics.log( msg );
Log.e( tag, msg );
// Now crash so Crashlytics will upload the log
new Thread( new Runnable() {
@Override
public void run() {