public FileData getFileData(Object fileKey, long start, long end, boolean[] includes,
      DiskRangeListFactory factory, LowLevelCacheCounters qfCounters, BooleanRef gotAllData)
          throws IOException {
    FileCache<FileData> subCache = cache.get(fileKey);
    if (subCache == null || !subCache.incRef()) {
      if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) {
        LlapIoImpl.CACHE_LOGGER.trace("Cannot find cache for " + fileKey + " in " + cache);
      }
      markAllAsMissed(start, end, qfCounters, gotAllData);
      return null;
    }

    try {
      FileData cached = subCache.getCache();
      cached.rwLock.readLock().lock();
      if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) {
        LlapIoImpl.CACHE_LOGGER.trace("Cache for " + fileKey + " is " + subCache.getCache());
      }
      try {
        if (cached.stripes == null) {
          LlapIoImpl.CACHE_LOGGER.debug("Cannot find any stripes for " + fileKey);
          markAllAsMissed(start, end, qfCounters, gotAllData);
          return null;
        }
        if (includes.length > cached.colCount) {
          throw new IOException("Includes " + DebugUtils.toString(includes) + " for "
              + cached.colCount + " columns");
        }
        FileData result = new FileData(conf, cached.fileKey, cached.colCount);
        if (gotAllData != null) {
          gotAllData.value = true;
        }
        // We will adjust start and end so that we could record the metrics; save the originals.
        long origStart = start, origEnd = end;
        // startIx is inclusive, endIx is exclusive.
        int startIx = Integer.MIN_VALUE, endIx = Integer.MIN_VALUE;
        LlapIoImpl.CACHE_LOGGER.debug("Looking for data between " + start + " and " + end);
        for (int i = 0; i < cached.stripes.size() && endIx == Integer.MIN_VALUE; ++i) {
          StripeData si = cached.stripes.get(i);
          if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) {
            LlapIoImpl.CACHE_LOGGER.trace("Looking at " + si.toCoordinateString());
          }

          if (startIx == i) {
            // The start of the split was in the middle of the previous slice.
            start = si.knownTornStart;
          } else if (startIx == Integer.MIN_VALUE) {
            // Determine if we need to read this slice for the split.
            if (si.lastEnd <= start) continue; // Slice before the start of the split.
            // Start of the split falls somewhere within or before this slice.
            // Note the ">=" - LineRecordReader will skip the first row even if we start
            // directly at its start, because it cannot know if it's the start or not.
            // Unless it's 0; note that we DO give 0 special treatment here, unlike the EOF below,
            // because zero is zero. Need to mention it in Javadoc.
            if (start == 0 && si.firstStart == 0) {
              startIx = i;
            } else if (start >= si.firstStart) {
              // If the start of the split points into the middle of the cached slice, we cannot
              // use the cached block - it's encoded and columnar, so we cannot map the file
              // offset to some "offset" in "middle" of the slice (but see TODO for firstStart).
              startIx = i + 1;
              // continue;
            } else {
              // Start of the split is before this slice.
              startIx = i; // Simple case - we will read cache from the split start offset.
              start = si.knownTornStart;
            }
          }

          // Determine if this (or previous) is the last slice we need to read for this split.
          if (startIx != Integer.MIN_VALUE && endIx == Integer.MIN_VALUE) {
            if (si.lastEnd <= end) {
              // The entire current slice is part of the split. Note that if split end EQUALS
              // lastEnd, the split would also read the next row, so we do need to look at the
              // next slice, if any (although we'd probably find we cannot use it).
              // Note also that we DO NOT treat end-of-file differently here, cause we do not know
              // of any such thing. The caller must handle lastEnd vs end of split vs end of file
              // match correctly in terms of how LRR handles them. See above for start-of-file.
              if (i + 1 != cached.stripes.size()) continue;
              endIx = i + 1;
              end = si.lastEnd;
            } else if (si.lastStart <= end) {
              // The split ends within (and would read) the last row of this slice. Exact match.
              endIx = i + 1;
              end = si.lastEnd;
            } else {
              // Either the slice comes entirely after the end of split (following a gap in cached
              // data); or the split ends in the middle of the slice, so it's the same as in the
              // startIx logic w.r.t. the partial match; so, we either don't want to, or cannot,
              // use this. There's no need to distinguish these two cases for now.
              endIx = i;
              end = (endIx > 0) ? cached.stripes.get(endIx - 1).lastEnd : start;
            }
          }
        }
        LlapIoImpl.CACHE_LOGGER.debug("Determined stripe indexes " + startIx + ", " + endIx);
        if (endIx <= startIx) {
          if (gotAllData != null) {
            gotAllData.value = false;
          }
          return null;  // No data for the split, or it fits in the middle of one or two slices.
        }
        if (start > origStart || end < origEnd) {
          if (gotAllData != null) {
            gotAllData.value = false;
          }
          long totalMiss = Math.max(0, origEnd - end) + Math.max(0, start - origStart);
          metrics.incrCacheRequestedBytes(totalMiss);
          if (qfCounters != null) {
            qfCounters.recordCacheMiss(totalMiss);
          }
        }

        result.stripes = new ArrayList<>(endIx - startIx);
        for (int stripeIx = startIx; stripeIx < endIx; ++stripeIx) {
          getCacheDataForOneSlice(stripeIx, cached, result, gotAllData, includes, qfCounters);
        }
        return result;
      } finally {
        cached.rwLock.readLock().unlock();
      }
    } finally {
      subCache.decRef();
    }
  }
RW lock ensures we have a consistent view of the file data, which is important given that
we generate "stripe" boundaries arbitrarily. Reading buffer data itself doesn't require
that this lock is held; however, everything else in stripes list does.
TODO: make more granular? We only care that each one reader sees consistent boundaries.
      So, we could shallow-copy the stripes list, then have individual locks inside each.
llap-server/src/java/org/apache/hadoop/hive/llap/cache/SerDeLowLevelCacheImpl.java