Flush join indexes when memory is constrained

Add a separate limit for index memory usage that is independent of the task
memory limit. This removes the risk of the flushable memory usage causing
another operator to run out of memory.

Loader will attempt to load new data and add to existing cache.  If that
fails, it flushes the cache and tries again.  If that fails, it loads only
the single required request for the loader thread.
There's a weird edge condition here. If we reset the indexSnapshotReference, we implicitly remove the cached values and negatively cached values from the snapshot, but we don't actually clear the underlying missingKeys reference. This means if we repeatedly fetch and clear the indexSnapshotReference, we could potentially keep appending the same keys to the missingKeys cache and use up all the memory.