apache/druid · error · SegmentLoadingException

Unable to copy key [%s] to file [%s]

Error message

Unable to copy key [%s] to file [%s]

What it means

After confirming equal lengths, mergeBloomFilterByteBuffers compares the header bytes (numHashFunctions and numBits fields before START_OF_SERIALIZED_LONGS) of both buffers byte-by-byte. Any difference in those header bytes means the filters use a different number of hash functions or bit layout, and merging would corrupt the result, so it throws this IllegalArgumentException. Note: this check compares every header byte, so even padding differences trip it.

Source

Thrown at extensions-contrib/cassandra-storage/src/main/java/org/apache/druid/storage/cassandra/CassandraDataSegmentPuller.java:82

    try {
      RetryUtils.retry(
          () -> {
            try (OutputStream os = new FileOutputStream(tmpFile)) {
              ChunkedStorage
                  .newReader(indexStorage, key, os)
                  .withBatchSize(BATCH_SIZE)
                  .withConcurrencyLevel(CONCURRENCY)
                  .call();
            }
            return new FileUtils.FileCopyResult(tmpFile);
          },
          Predicates.alwaysTrue(),
          10
      );
    }
    catch (Exception e) {
      throw new SegmentLoadingException(e, "Unable to copy key [%s] to file [%s]", key, tmpFile.getAbsolutePath());
    }
    try {
      final FileUtils.FileCopyResult result = CompressionUtils.unzip(tmpFile, outDir);
      log.info(
          "Pull of file[%s] completed in %,d millis (%s bytes)", key, System.currentTimeMillis() - startTime,
          result.size()
      );
      return result;
    }
    catch (Exception e) {
      try {
        FileUtils.deleteDirectory(outDir);
      }
      catch (IOException e1) {
        log.error(e1, "Error clearing segment directory [%s]", outDir.getAbsolutePath());
        e.addSuppressed(e1);
      }
      throw new SegmentLoadingException(e, e.getMessage());

View on GitHub (pinned to 9b90983fd2)

Solutions

  1. Standardize bloomFilterNumHashFunctions (and numBits) across all producers of filters that will be merged.
  2. Deserialize both filters and verify getNumHashFunctions() matches before merging.
  3. Double-check bf1Start/bf2Start offsets so header comparisons are aligned to actual filter starts.
  4. Catch IllegalArgumentException and fall back to treating the merge as unsupported for those inputs.

Example fix

// before
// query A: bloomFilterNumHashFunctions=3, query B: bloomFilterNumHashFunctions=5
BloomKFilter.mergeBloomFilterByteBuffers(bufA, bufB, 0, 0); // throws

// after
// use one shared config for all stages
Query a = base.clone().withOverriddenContext(ImmutableMap.of("bloomFilterNumHashFunctions", 4, "bloomFilterNumBits", 1024));
Query b = base.clone().withOverriddenContext(ImmutableMap.of("bloomFilterNumHashFunctions", 4, "bloomFilterNumBits", 1024));
// now merge is safe
Defensive patterns

Strategy: validation

Validate before calling

boolean sameHeader(ByteBuffer b1, int s1, ByteBuffer b2, int s2) {
  for (int i = 0; i < BloomKFilter.START_OF_SERIALIZED_LONGS; i++) {
    if (b1.get(s1 + i) != b2.get(s2 + i)) return false;
  }
  return true;
}

Type guard

boolean isCompatibleMergeInput(ByteBuffer b1, int s1, ByteBuffer b2, int s2) {
  return sameHeader(b1, s1, b2, s2) && serializedLen(b1, s1) == serializedLen(b2, s2);
}

Try / catch

try {
  BloomKFilter.mergeBloomFilterByteBuffers(bf1Buffer, bf2Buffer, start1, start2);
} catch (IllegalArgumentException e) {
  if (e.getMessage().contains("NumHashFunctions/NumBits")) {
    throw new IllegalStateException("Filters use different hash-function counts; standardize bloomFilterNumHashFunctions", e);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling mergeBloomFilterByteBuffers with two buffers of the same serialized length but different numHashFunctions/numBits header values — e.g. same numBits size in bytes but different hash function counts, or header bytes stored incorrectly.

Common situations: Filters built with identical bit sizes but different bloomFilterNumHashFunctions; endianness or version differences in how headers were written; merging buffers where one start offset points at the wrong byte, comparing misaligned headers.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of apache/druid@9b90983fd2 (2026-09-07). Data as JSON: /api/errors/933bbffc977a9dc5. Report an issue: GitHub.