CouchbaseLite.init() IllegalStateException: Tmp dir root is null

We have a user that experiences this crash repeatedly:

Caused by java.lang.IllegalStateException: Tmp dir root is null
       at com.couchbase.lite.internal.CouchbaseLiteInternal.makeTmpPath(CouchbaseLiteInternal.java:152)
       at com.couchbase.lite.internal.CouchbaseLiteInternal.initDirectories(CouchbaseLiteInternal.java:206)
       at com.couchbase.lite.internal.CouchbaseLiteInternal.init(CouchbaseLiteInternal.java:94)
       at com.couchbase.lite.CouchbaseLite.init(CouchbaseLite.java:53)
       at com.couchbase.lite.CouchbaseLite.init(CouchbaseLite.java:35)

Looking at the code:

@NonNull
public static String makeTmpPath(@Nullable String rootDir) {
    requireInit("Can't create tmp dir path");
    final File dir = (rootDir != null)
            ? new File(rootDir, TEMP_DIR_NAME)
            : getContext().getExternalFilesDir(TEMP_DIR_NAME);
    if (dir == null) { throw new IllegalStateException("Tmp dir root is null"); }
    return verifyDir(dir);
}

getExternalFilesDir() is documented “May return null if shared storage is not currently available.” I’d expect there should be a proper fallback to be able to create a tmp directory on internal storage if external storage is unavailable instead of crashing the app in this case. Or possibly just create it on internal storage in the first place, which is always available.

For example:

@NonNull
public static String makeTmpPath(@Nullable String rootDir) {
    requireInit("Can't create tmp dir path");
    File dir = (rootDir != null)
            ? new File(rootDir, TEMP_DIR_NAME)
            : getContext().getExternalFilesDir(TEMP_DIR_NAME);
    if (dir == null) {
        dir = getContext().getDir(TEMP_DIR_NAME, Context.MODE_PRIVATE);
    }
    return verifyDir(dir);
}
1 Like

Hey @jeff.lockhart
I would prefer not to do that. That code would present the threat of filling the sandbox space, without warning the application. It would make a lot more sense for an app that may run on the rare system that does not emulate external storage, to pass in a rootDir, of its choice.

The use of that tmp dir has been a bugaboo of mine for a while. In the next release of CBL, it will go away.

1 Like

That’s good news it will be going away, and this bug with it.

For most apps, the devices users choose to run on are out of the developer’s control though. So while it may be rare, I’d consider it an important case to handle. It could happen on a device with an SD card and the user takes the SD card out. This is the device the user is crashing on. Looks like it just doesn’t even come with an SD card, which may not be uncommon. On such a device, with no external storage as an option, the internal storage is really the only option. When faced with the choice between crashing the app or using internal storage, the answer should be pretty straightforward, which is what my suggested code would do.

Ultimately if a system API can return null, then the case should be handled, otherwise it would hardly ever make sense for a developer to use the CouchbaseLite.init() base function, except in the rare case where they happen to be in control of all devices that will ever run their app. In every other case, the developer would want to implement logic similar to my suggested change to prevent potential app crashing.

Wow… What a device!

I will say that I have never heard of a device that does not have an SD card, that does not emulate SD storage by mounting an internal partition on /sdcard. Very odd.

None the less. I agree that it is something that some apps are going to have to watch out for. The library certainly shouldn’t be throwing an exception for something that is documented behavior.

I also think… though, that unexpectedly using internal storage is nearly as bad as throwing the exception. Making guesses about how to handle situations that we don’t expect, is just not right.

I suppose you could catch the exception and retry with a specified directory :stuck_out_tongue_winking_eye:

Yeah, I’ll probably do that in the meantime until the next update removes this entirely. Is this tmp directory only used for a cache? Is it problematic if a user were to have it mounted on external storage and on a different app launch found the volume unavailable, so created a new directory on internal storage? What files are stored in this tmp directory?

I’m not sure all the different scenarios that the external storage isn’t available, only that it’s documented as potentially unavailable through the API. I know it used to be unavailable if mounted on a computer through USB. I think the current default storage transfer protocol avoids this though.

In the scenario when external storage is unavailable, other than using internal storage, what other option would a developer have to avoid crashing?

In the scenario when external storage is unavailable, other than using internal storage, what other option would a developer have to avoid crashing?

Well… who knows!!

The only place I have ever encountered no external storage, before, is an explicitly unmounted SD card. Since, maybe, Honeycomb, there’s even been a distinction between “external” and unmountable storage (commonly a sub-directory of /sdcard). As I say, I have never seen a device that doesn’t have a mountable storage, and also doesn’t emulate it with a partition from the internal persistent storage.

I am a strong advocate of not pre-empting the superior understanding of downstream developers. You, for instance, are encountering the situation and you know exactly what is going on, in your case. I have absolutely no idea. Better you decide, than I do.

Currently, the only use of that tmp/scratch directory is that it is passed to Core. I had a discussion with @borrrden , our Core guy, about this before I removed it. I think that, at one time, Core passed the directory along to SQLite, for use as a tmp dir. I, actually, am not sure what SQLite uses it for.

Ok. Well, that would be an argument for why I’d expect this to be a decision the library made, since as a developer I would need to know answers to these questions in order to make an informed decision about where to allocate storage. :wink: I understand the caution though.

Sounds like this isn’t actually being used anymore, as of the next release though, so that’s good news, since it simplifies things for all.

1 Like

By the way, this has happened to 6 users now, running these devices:

Galaxy Note10+ - Android 11
Galaxy S8+ - Android 7.1.2
Ellipsis 8 HD - Android 7.0
Xperia Z2 Tablet - Android 6.0.1
Nokia 3 V - Android 9
Lenovo YOGA Tab 3 Plus - Android 7.1.1

Looking at this closer, a developer workaround to avoid this crash would only work consistently if the manual root directory was context.getFilesDir(). This is because this is what makeDbPath() uses by default for the database path if rootDirectory == null. So if the user has an SD card in their device at one point, the default init() will create the database in the getFilesDir(). Later, if they’ve removed the SD card and external storage is not available, in order to prevent the app crashing, the developer’s only choice is to catch the exception and pass context.getFilesDir() as the rootDirectory parameter. context.getCacheDir() or any other directory can’t be used, as it will attempt to find the database in that same directory and not locate the existing database.

This has the drawback of not storing these temp files in a cache directory, which can be cleared by the OS or user to free up storage without affecting application state. Typically this cache directory would be a better volume for temporary storage usage than even external storage is on modern Android devices, which typically have considerable internal storage vs what’s available on an external SD card. Besides adding the ability to clear the cache when desired through application settings, it also is more secure in the application sandbox, not publicly accessible like external storage. Again, without knowing how this temp directory is even used, what types of files get created there, it’s possible the data might contain sensitive document data.

For this reason, a developer workaround for this crash really doesn’t make sense.

@jeff.lockhart :
Suggest you use

public static void init(@NonNull Context ctxt, @Nullable File rootDirectory)

… which will put the tmp directory at the top level of the passed directory. In the upcoming versions of CBL, you will be able to specify the locations of the default db directory and SQLite’s scratch directory, separately.

Yes, that’s the function I was using, which uses the same directory for both database and cache.

Is the temp directory going away or just more granular configuration coming in next version of Couchbase Lite? Are these changes in Couchbase Lite 3.x?

If you specify a directory for a database, it will be put in the specified directory. A root directory specified in the init method determines the location of the cache but need not determine the location of a database.

SQLite and LiteCore both need scratch directories. For reasons that probably are related to how CBL works on other platforms, those two directories and the default location of a database have been unnecessarily intertwined.

As of 3.x, you can specify two separate directories in the call to init. The first is the default directory for databases. The second is SQLite’s scratch directory. LiteCore’s scratch directory will always be a sub-directory of the default database directory.

Great, thanks for clarifying. That makes more sense. Looking forward to many of the new changes in 3.x. Do you have an estimate for when it will be out of beta?

I believe we announced a public beta, last week.

I did see the public beta. Haven’t had a chance to test it out yet, but was wondering what the timeline is for it to be out of beta for a final release.

… and I, of course, cannot say a single thing about that. Way outta my wheelhouse. It shouldn’t be too long, tho…

1 Like

Any update on this? Firebase shows that over the last 90 days we have 319 crash events affecting 53 users. Looks like over 15 different phone manufacturers and multiple phones per manufacturer (including: Motorola, Samsung, LGE, OnePlus, etc) . (couchbase-lite-android:2.8.6)

1 Like

I think that all of the information you need to cope with this issue, is in this ticket.
Before calling CouchbaseLite.init, check to see if external storage is available. If it is not, specify some other location as a default/scratch directory. As I say in the ticket, there is no good way for me to guess where that might be, on any give device. You’ll need to find something that will work for your app on the target device.

@jeff.lockhart suggests the sandbox. That makes sense to me. You’ll just need to watch your use of space. Perhaps you could just always specify the sandbox and skip the check.

So basically something like this? (if I want things to work as intended for the majority of people, but not have the couchbase library crash the app for the 53 users)

if (context.getExternalFilesDir("CouchbaseLiteTemp") == null) {
     CouchbaseLite.init(context, context.filesDir)
} else {
     CouchbaseLite.init(context)
}

Exactly. How about:

CouchbaseLite.init(
    context,
    context.getExternalFilesDir(".tmp") ?: context.filesDir))