6 Replies Latest reply on Jun 18, 2015 1:36 AM by Mikkel Kjeldsen

    Rare race condition causes scanner death on TC55 with EMDK

    Mikkel Kjeldsen

      I'm using a TC55 model with the EMDK 3.0 U1 OS upgrade manually applied. I have confirmed this issue on around 10 devices.

       

      The default barcode sample project acquires and enables a com.symbol.emdk.barcode.Scanner instance on the UI thread. This makes the sample work, but causes extreme jankiness; it is not a viable approach.

       

      I have provided a gist of, and added to the end of this post, changes to the (Android Studio) BarcodeSample1 project that demonstrate calling Scanner#enable() on a background thread. I tried to produce a proper patch but CRLF gave me too much trouble, sorry.

       

      The patch uses Android's AsyncTask pattern, but any implementation (e.g. executors) will produce the same result. AsyncTask was chosen because the patch stays small and no extra imports are required.

      Unfortunately, there is a race condition. If the application is paused in-between scheduling the task responsible for calling #enable() and the completion of that task, #enable() fails. The precise failure varies from time to time; example exceptions are listed at the bottom.

       

      Often, #enable() can simply be tried again, however, on rare occasions, the failure will be of such a nature that no app is able to interact with the scanner hardware at all — not even DWDemo, which does not even use EMDK OS 3.0-U1 (DWDemo will simply do nothing when the user attempts to  start the scanner). When this happens, the only solution I found has been to completely restart the device.

       

      The patch creates the circumstances under which the race condition can occur but does not itself trigger it. To do so, start the app, then repeatedly hit the Back or Home button until the app has been backgrounded. If the error occurred, within a few seconds Android will create an alert dialog. Otherwise, repeat this process.

       

      The following methods are known to require delegation and exhibit this issue:

      • Scanner#enable()
      • Scanner#getConfig()
      • Scanner#setConfig(com.symbol.emdk.barcode.ScannerConfig)

      The list may not be exhaustive.

       

      There are three serious problems with this:

      • Failure to enable a scanner may be indicative of a need to restart the device.
      • It is impossible to detect when a restart is necessary.
      • It is both impossible and undesirable to prevent an app from pausing.

      The only thing I can do to work around this right now is to alert the user after numerous successive failures.

       

      Does anyone know what's going on and how best to deal with it?

       

      Example exceptions:

       

      com.symbol.emdk.barcode.ScannerException: Failed to register for the hard trigger key notification.
          at com.symbol.emdk.barcode.Scanner.enable(Scanner.java:323)
      
      com.symbol.emdk.barcode.ScannerException: Scanner initialization failed.
          at com.symbol.emdk.barcode.Scanner.enable(Scanner.java:323)
      
      com.symbol.emdk.barcode.ScannerException: Failure
          at com.symbol.emdk.barcode.Scanner.getConfig(Scanner.java:213)
      

       

      MainActivity.java:

       

            if (scanner != null) {
            
              scanner.addDataListener(this);
              scanner.addStatusListener(this);
      
              new AsyncEnable().execute(scanner);
            }
          }
        }
      
        private static final class AsyncEnable extends AsyncTask<Scanner, Void, Void> {
          @Override
          protected Void doInBackground(final Scanner... params) {
            try {
              params[0].enable();
            } catch (ScannerException e) {
              e.printStackTrace();
              throw new RuntimeException(e);
            }
            return null;
          }
        }
      
        private void deInitScanner() {
      

       

      Patch:

       

      diff --git a/app/src/main/java/com/symbol/barcodesample1/MainActivity.java b/app/src/main/java/com/symbol/barcodesample1/MainActivity.java
      index bdfaa30..1409d63 100644
      --- a/app/src/main/java/com/symbol/barcodesample1/MainActivity.java
      +++ b/app/src/main/java/com/symbol/barcodesample1/MainActivity.java
      @@ -516,13 +516,21 @@ public class MainActivity extends Activity implements EMDKListener, DataListener
              scanner.addDataListener(this);
              scanner.addStatusListener(this);
       
      +                               new AsyncEnable().execute(scanner);
      +                       }
      +               }
      +       }
      +
      +       private static final class AsyncEnable extends AsyncTask<Scanner, Void, Void> {
      +               @Override
      +               protected Void doInBackground(final Scanner... params) {
            try {
      -                                       scanner.enable();
      +                               params[0].enable();
            } catch (ScannerException e) {
      -                               
      -                                       textViewStatus.setText("Status: " + e.getMessage());
      -                               }
      +                               e.printStackTrace();
      +                               throw new RuntimeException(e);
            }
      +                       return null;
          }
        }
      
        • Re: Rare race condition causes scanner death on TC55 with EMDK
          Mikkel Kjeldsen

          I have accidentally made some progress on this.

           

          The underlying cause of the problem is still unknown and unresolved, and I am nearly convinced that it is internal to EMDK 3.0 U1 or the (Android 4.1.2) TC55 device. However, in an attempt to intercept the issue and display useful feedback, I discovered that repeating the attempt to acquire the scanner often (always) succeeds. Since I don't actually use the AsyncTask pattern but executors I won't be building on the code samples included in the original post.

           

          In EMDKManager.EMDKListener#onOpened, and later on in Activity#onResume, I call #acquireHardwareScanner():

           

          private void acquireHardwareScanner() {
              if (scanner != null) {
                  return;
              }
          
              if (BuildConfig.DEBUG) {
                  checkState(barcodeManager != null, "barcodeManager == null");
              }
          
              synchronized (lock) {
                  if (scanner == null) {
                      final Scanner s = scanner = barcodeManager
                              .getDevice(BarcodeManager.DeviceIdentifier.DEFAULT);
          
                      if (s == null) {
                          throw new RuntimeException("Null scanner returned");
                      }
          
                      s.addDataListener(this);
                      s.addStatusListener(this);
          
                      scheduleScannerAcquisition();
                  }
              }
          }
          
          private void scheduleScannerAcquisition() {
              if (BuildConfig.DEBUG) {
                  checkState(scanner != null, "scanner == null");
              }
          
              final ListenableFuture<Optional<Scanner>> futureScanner =
                      scannerExecutor.submit(DefaultScannerPreparation.of(scanner));
          
              Futures.addCallback(futureScanner,
                      new EnabledScannerUICallback(this),
                      new MainThreadExecutor());
          }
          

           

          DefaultScannerPreparation is a Callable that makes calls to #enable(), #getConfig(), and setConfig(ScannerConfig); it returns an Optional<Scanner>.

           

          I use Guava's Futures to schedule a FutureCallback<Optional<Scanner>>. This can be done using plain java.util.Futures, but it's more boilerplate and I already had a dependency on Guava.

           

          The callback's onSuccess(Optional<Scanner>) is uninteresting; it only arranges some fragments. onFailure(Throwable)looks like this:

           

          @Override
          public void onFailure(@NonNull final Throwable t) {
              if (BuildConfig.DEBUG) {
                  t.printStackTrace();
              }
          
              final MainActivity main = weakMainActivity.get();
          
              if (main == null || main.isFinishing()) {
                  return;
              }
          
              if (++consecutiveAcquisitionFailures % 3 == 0) {
                  allowUserToBail(main);
              } else {
                  main.scheduleScannerAcquisition();
              }
          }
          

           

          The most important part here is the call to #scheduleScannerAcquisition(), which submits another attempt to acquire the scanner in nearly the same way. The only difference is that it skips the preliminary setup, including the call to BarcodeManager#getDevice(BarcodeManger.DeviceIdentifier) and the calls to #addDataListener and #addStatusListener.

           

          To test this, I manually put the device into the problematic state using the method explained in the original post. The situation now is that the first attempt to acquire the scanner, whether it be called from #onOpened or #onResume, always fails, and always in the same way as mentioned in the original post. The automatic retry then succeeds, and I have no further trouble interacting with the scanner. This sequence will repeat itself whenever the app is brought into the foreground until I reboot the device.

           

          • I have not yet experienced a failure streak of 2 or more. That is to say, the first retry always succeeds. However, I have very little data so far, so I built in a failsafe that prevents a possible lock.
          • I have confirmed with certainty that the problem is not directly caused by any of #getDevice, #addDataListener, or #addStatusListener, nor the timing of them. If they play a role in the issue, everything suggests that this is not in userland.
          • I have not investigated whether delaying #acquireHardwareScanner() makes the underlying issue disappear.

           

          I am still interested in knowing why this happens and if it can be avoided completely, but so far this work-around seems usable.

          1 of 1 people found this helpful