All Downloads are FREE. Search and download functionalities are using the official Maven repository.

dev.responsive.kafka.internal.db.inmemory.InMemoryKVTable Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2024 Responsive Computing, Inc.
 *
 * This source code is licensed under the Responsive Business Source License Agreement v1.0
 * available at:
 *
 * https://www.responsive.dev/legal/responsive-bsl-10
 *
 * This software requires a valid Commercial License Key for production use. Trial and commercial
 * licenses can be obtained at https://www.responsive.dev
 */

package dev.responsive.kafka.internal.db.inmemory;

import com.datastax.oss.driver.api.core.cql.BoundStatement;
import dev.responsive.kafka.api.stores.TtlProvider.TtlDuration;
import dev.responsive.kafka.internal.db.KVFlushManager;
import dev.responsive.kafka.internal.db.RemoteKVTable;
import dev.responsive.kafka.internal.db.RemoteWriter;
import dev.responsive.kafka.internal.db.partitioning.TablePartitioner;
import dev.responsive.kafka.internal.stores.RemoteWriteResult;
import dev.responsive.kafka.internal.stores.ResponsiveStoreRegistration;
import dev.responsive.kafka.internal.stores.TtlResolver;
import dev.responsive.kafka.internal.utils.Iterators;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.apache.kafka.common.utils.Bytes;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.state.KeyValueIterator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// Note: this class doesn't actually use BoundStatements and applies all operations immediately,
// we just stub the BoundStatement type so we can reuse this table for the TTD
public class InMemoryKVTable implements RemoteKVTable {
  private static final Logger LOG = LoggerFactory.getLogger(InMemoryKVTable.class);

  private int kafkaPartition;
  private final String name;
  private final Optional> ttlResolver;
  private final ConcurrentNavigableMap store = new ConcurrentSkipListMap<>();

  public InMemoryKVTable(final String name, final Optional> ttlResolver) {
    this.name = Objects.requireNonNull(name);
    this.ttlResolver = ttlResolver;
  }

  @Override
  public KVFlushManager init(int kafkaPartition) {
    LOG.info("init in-memory kv store {} {}", name, kafkaPartition);
    this.kafkaPartition = kafkaPartition;
    return new InMemoryKVFlushManager();
  }

  @Override
  public byte[] get(final int kafkaPartition, final Bytes key, final long streamTimeMs) {
    checkKafkaPartition(kafkaPartition);
    final var value = store.get(key);
    if (value == null) {
      return null;
    }

    if (ttlResolver.isPresent()) {
      final TtlDuration rowTtl = ttlResolver.get().resolveTtl(key, value.value());
      if (rowTtl.isFinite()) {
        final long minValidTs = streamTimeMs - rowTtl.toMillis();
        if (value.epochMillis < minValidTs) {
          return null;
        }
      }
    }

    return value.value();
  }

  @Override
  public KeyValueIterator range(
      final int kafkaPartition,
      final Bytes from,
      final Bytes to,
      final long streamTimeMs
  ) {
    if (ttlResolver.isPresent() && !ttlResolver.get().hasDefaultOnly()) {
      throw new UnsupportedOperationException("Row-level ttl is not yet supported for range "
                                                  + "queries on in-memory tables or TTD");
    }

    checkKafkaPartition(kafkaPartition);

    final var iter = store
        .tailMap(from, true)
        .headMap(to, true)
        .entrySet()
        .iterator();

    final long minValidTs = ttlResolver.isEmpty()
        ? -1L
        : streamTimeMs - ttlResolver.get().defaultTtl().toMillis();

    return iteratorWithTimeFilter(iter, minValidTs);
  }

  @Override
  public KeyValueIterator all(final int kafkaPartition, final long streamTimeMs) {
    if (ttlResolver.isPresent() && !ttlResolver.get().hasDefaultOnly()) {
      throw new UnsupportedOperationException("Row-level ttl is not yet supported for range "
                                                  + "queries on in-memory tables or TTD");
    }

    checkKafkaPartition(kafkaPartition);
    final var iter = store.entrySet().iterator();

    final long minValidTs = ttlResolver.isEmpty()
        ? -1L
        : streamTimeMs - ttlResolver.get().defaultTtl().toMillis();

    return iteratorWithTimeFilter(iter, minValidTs);
  }

  @Override
  public long approximateNumEntries(int kafkaPartition) {
    checkKafkaPartition(kafkaPartition);
    return store.size();
  }

  @Override
  public String name() {
    return name;
  }

  @Override
  public BoundStatement insert(int kafkaPartition, Bytes key, byte[] value, long epochMillis) {
    checkKafkaPartition(kafkaPartition);

    store.put(key, new Value(epochMillis, value));
    return null;
  }

  @Override
  public BoundStatement delete(int kafkaPartition, Bytes key) {
    checkKafkaPartition(kafkaPartition);
    store.remove(key);

    return null;
  }

  @Override
  public long fetchOffset(int kafkaPartition) {
    checkKafkaPartition(kafkaPartition);
    return ResponsiveStoreRegistration.NO_COMMITTED_OFFSET;
  }

  private KeyValueIterator iteratorWithTimeFilter(
      final Iterator> iter,
      final long minValidTs
  ) {
    return Iterators.kv(
        Iterators.filter(
            Iterators.kv(iter, e -> new KeyValue<>(e.getKey(), e.getValue())),
            kv -> kv.value.epochMillis >= minValidTs
        ),
        kv -> new KeyValue<>(kv.key, kv.value.value())
    );
  }

  public class InMemoryKVFlushManager extends KVFlushManager {
    private InMemoryKVFlushManager() {
    }

    @Override
    public RemoteWriteResult updateOffset(long consumedOffset) {
      return RemoteWriteResult.success(kafkaPartition);
    }

    @Override
    public String tableName() {
      return name;
    }

    @Override
    public TablePartitioner partitioner() {
      return TablePartitioner.defaultPartitioner();
    }

    @Override
    public RemoteWriter createWriter(Integer tablePartition) {
      return new RemoteWriter<>() {
        @Override
        public void insert(Bytes key, byte[] value, long epochMillis) {
          InMemoryKVTable.this.insert(tablePartition, key, value, epochMillis);
        }

        @Override
        public void delete(Bytes key) {
          InMemoryKVTable.this.delete(tablePartition, key);
        }

        @Override
        public CompletionStage> flush() {
          return CompletableFuture.completedFuture(RemoteWriteResult.success(kafkaPartition));
        }
      };
    }

    @Override
    public String failedFlushInfo(long batchOffset, Integer failedTablePartition) {
      return "failed flush";
    }

    @Override
    public String logPrefix() {
      return "inmemory";
    }
  }

  private void checkKafkaPartition(int kafkaPartition) {
    if (this.kafkaPartition != kafkaPartition) {
      throw new IllegalStateException(
          "unexpected partition: " + kafkaPartition + " for " + this.kafkaPartition);
    }
  }

  private static class Value {
    private final long epochMillis;
    private final byte[] value;

    public Value(final long epochMillis, final byte[] value) {
      this.epochMillis = epochMillis;
      this.value = value;
    }

    public long epochMillis() {
      return epochMillis;
    }

    public byte[] value() {
      return value;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy