/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.facebook.presto.redis; import com.facebook.presto.decoder.DecoderColumnHandle; import com.facebook.presto.decoder.FieldDecoder; import com.facebook.presto.decoder.FieldValueProvider; import com.facebook.presto.decoder.RowDecoder; import com.facebook.presto.spi.RecordCursor; import com.facebook.presto.spi.type.Type; import com.google.common.base.Throwables; import io.airlift.log.Logger; import io.airlift.slice.Slice; import io.airlift.slice.Slices; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.ScanParams; import redis.clients.jedis.ScanResult; import java.nio.charset.StandardCharsets; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import static com.facebook.presto.redis.RedisInternalFieldDescription.KEY_CORRUPT_FIELD; import static com.facebook.presto.redis.RedisInternalFieldDescription.KEY_FIELD; import static com.facebook.presto.redis.RedisInternalFieldDescription.KEY_LENGTH_FIELD; import static com.facebook.presto.redis.RedisInternalFieldDescription.VALUE_CORRUPT_FIELD; import static com.facebook.presto.redis.RedisInternalFieldDescription.VALUE_FIELD; import static com.facebook.presto.redis.RedisInternalFieldDescription.VALUE_LENGTH_FIELD; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; import static redis.clients.jedis.ScanParams.SCAN_POINTER_START; public class RedisRecordCursor implements RecordCursor { private static final Logger log = Logger.get(RedisRecordCursor.class); private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; private final RowDecoder keyDecoder; private final RowDecoder valueDecoder; private final Map<DecoderColumnHandle, FieldDecoder<?>> keyFieldDecoders; private final Map<DecoderColumnHandle, FieldDecoder<?>> valueFieldDecoders; private final RedisSplit split; private final List<DecoderColumnHandle> columnHandles; private final RedisJedisManager redisJedisManager; private final JedisPool jedisPool; private final ScanParams scanParms; private ScanResult<String> redisCursor; private Iterator<String> keysIterator; private final AtomicBoolean reported = new AtomicBoolean(); private FieldValueProvider[] fieldValueProviders; private String valueString; private Map<String, String> valueMap; private long totalBytes; private long totalValues; RedisRecordCursor( RowDecoder keyDecoder, RowDecoder valueDecoder, Map<DecoderColumnHandle, FieldDecoder<?>> keyFieldDecoders, Map<DecoderColumnHandle, FieldDecoder<?>> valueFieldDecoders, RedisSplit split, List<DecoderColumnHandle> columnHandles, RedisJedisManager redisJedisManager) { this.keyDecoder = keyDecoder; this.valueDecoder = valueDecoder; this.keyFieldDecoders = keyFieldDecoders; this.valueFieldDecoders = valueFieldDecoders; this.split = split; this.columnHandles = columnHandles; this.redisJedisManager = redisJedisManager; this.jedisPool = redisJedisManager.getJedisPool(split.getNodes().get(0)); this.scanParms = setScanParms(); fetchKeys(); } @Override public long getTotalBytes() { return totalBytes; } @Override public long getCompletedBytes() { return totalBytes; } @Override public long getReadTimeNanos() { return 0; } @Override public Type getType(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); return columnHandles.get(field).getType(); } public boolean hasUnscannedData() { if (redisCursor == null) { return false; } // no more keys are unscanned when // when redis scan command // returns 0 string cursor return (!redisCursor.getStringCursor().equals("0")); } @Override public boolean advanceNextPosition() { while (!keysIterator.hasNext()) { if (!hasUnscannedData()) { return endOfData(); } fetchKeys(); } return nextRow(keysIterator.next()); } private boolean endOfData() { if (!reported.getAndSet(true)) { log.debug("Read a total of %d values with %d bytes.", totalValues, totalBytes); } return false; } private boolean nextRow(String keyString) { fetchData(keyString); byte[] keyData = keyString.getBytes(StandardCharsets.UTF_8); byte[] valueData = EMPTY_BYTE_ARRAY; if (valueString != null) { valueData = valueString.getBytes(StandardCharsets.UTF_8); } totalBytes += valueData.length; totalValues++; Set<FieldValueProvider> fieldValueProviders = new HashSet<>(); fieldValueProviders.add(KEY_FIELD.forByteValue(keyData)); fieldValueProviders.add(VALUE_FIELD.forByteValue(valueData)); fieldValueProviders.add(KEY_LENGTH_FIELD.forLongValue(keyData.length)); fieldValueProviders.add(VALUE_LENGTH_FIELD.forLongValue(valueData.length)); fieldValueProviders.add(KEY_CORRUPT_FIELD.forBooleanValue(keyDecoder.decodeRow( keyData, null, fieldValueProviders, columnHandles, keyFieldDecoders))); fieldValueProviders.add(VALUE_CORRUPT_FIELD.forBooleanValue(valueDecoder.decodeRow( valueData, valueMap, fieldValueProviders, columnHandles, valueFieldDecoders))); this.fieldValueProviders = new FieldValueProvider[columnHandles.size()]; // If a value provider for a requested internal column is present, assign the // value to the internal cache. It is possible that an internal column is present // where no value provider exists (e.g. the '_corrupt' column with the DummyRowDecoder). // In that case, the cache is null (and the column is reported as null). for (int i = 0; i < columnHandles.size(); i++) { for (FieldValueProvider fieldValueProvider : fieldValueProviders) { if (fieldValueProvider.accept(columnHandles.get(i))) { this.fieldValueProviders[i] = fieldValueProvider; break; } } } // Advanced successfully. return true; } @SuppressWarnings("SimplifiableConditionalExpression") @Override public boolean getBoolean(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); checkFieldType(field, boolean.class); return isNull(field) ? false : fieldValueProviders[field].getBoolean(); } @Override public long getLong(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); checkFieldType(field, long.class); return isNull(field) ? 0L : fieldValueProviders[field].getLong(); } @Override public double getDouble(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); checkFieldType(field, double.class); return isNull(field) ? 0.0d : fieldValueProviders[field].getDouble(); } @Override public Slice getSlice(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); checkFieldType(field, Slice.class); return isNull(field) ? Slices.EMPTY_SLICE : fieldValueProviders[field].getSlice(); } @Override public boolean isNull(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); return fieldValueProviders[field] == null || fieldValueProviders[field].isNull(); } @Override public Object getObject(int field) { checkArgument(field < columnHandles.size(), "Invalid field index"); throw new IllegalArgumentException(format("Type %s is not supported", getType(field))); } private void checkFieldType(int field, Class<?> expected) { Class<?> actual = getType(field).getJavaType(); checkArgument(actual == expected, "Expected field %s to be type %s but is %s", field, expected, actual); } @Override public void close() { } private ScanParams setScanParms() { if (split.getKeyDataType() == RedisDataType.STRING) { ScanParams scanParms = new ScanParams(); scanParms.count(redisJedisManager.getRedisConnectorConfig().getRedisScanCount()); // when Redis key string follows "schema:table:*" format // scan command can efficiently query tables // by returning matching keys // the alternative is to set key-prefix-schema-table to false // and treat entire redis as single schema , single table // redis Hash/Set types are to be supported - they can also be // used to filter out table data // "default" schema is not prefixed to the key if (redisJedisManager.getRedisConnectorConfig().isKeyPrefixSchemaTable()) { String keyMatch = ""; if (!split.getSchemaName().equals("default")) { keyMatch = split.getSchemaName() + Character.toString(redisJedisManager.getRedisConnectorConfig().getRedisKeyDelimiter()); } keyMatch = keyMatch + split.getTableName() + Character.toString(redisJedisManager.getRedisConnectorConfig().getRedisKeyDelimiter()) + "*"; scanParms.match(keyMatch); } return scanParms; } return null; } // Redis keys can be contained in the user-provided ZSET // Otherwise they need to be found by scanning Redis private boolean fetchKeys() { try (Jedis jedis = jedisPool.getResource()) { switch (split.getKeyDataType()) { case STRING: { String cursor = SCAN_POINTER_START; if (redisCursor != null) { cursor = redisCursor.getStringCursor(); } log.debug("Scanning new Redis keys from cursor %s . %d values read so far", cursor, totalValues); redisCursor = jedis.scan(cursor, scanParms); List<String> keys = redisCursor.getResult(); keysIterator = keys.iterator(); } break; case ZSET: Set<String> keys = jedis.zrange(split.getKeyName(), split.getStart(), split.getEnd()); keysIterator = keys.iterator(); break; default: log.debug("Redis type of key %s is unsupported", split.getKeyDataFormat()); return false; } } catch (Exception e) { throw Throwables.propagate(e); } return true; } private boolean fetchData(String keyString) { valueString = null; valueMap = null; // Redis connector supports two types of Redis // values: STRING and HASH // HASH types requires hash row decoder to // fill in the columns // whereas for the STRING type decoders are optional try (Jedis jedis = jedisPool.getResource()) { switch (split.getValueDataType()) { case STRING: valueString = jedis.get(keyString); if (valueString == null) { log.warn("Redis data modified while query was running, string value at key %s deleted", keyString); return false; } break; case HASH: valueMap = jedis.hgetAll(keyString); if (valueMap == null) { log.warn("Redis data modified while query was running, hash value at key %s deleted", keyString); return false; } break; default: log.debug("Redis type for key %s is unsupported", keyString); return false; } } catch (Exception e) { throw Throwables.propagate(e); } return true; } }