Post

Cómo recuperar un long de HBase y no morir en el intento

¿Recordáis cuando iba a intentar ser consistente a la hora de escribir en el blog? .

En fin, hoy os vengo a hablar de un detalle que me rompió la cabeza durante una temporada y he conseguido solucionar de manera relativamente eficaz:

Quien haya trabajo con HBase y su shell, se habrá encontrado con los siguientes datos alguna vez:

ROW            COLUMN+CELL
111222333      column=FAM1:ID, timestamp=1611977689866, value=111222333
111222333      column=FAM1:DAT1, timestamp=1611977689866, value=DAT1
111222333      column=FAM1:DAT2, timestamp=1611977689866, value=\x00\x00\x01t\xD1\x93\x1Fp

¿Y qué demonios es lo que pone ahí? Vais a vuestro código que se ha encargado de insertar un registro en HBase y véis que lo habéis guardado como un Long (que en Java ocupa 8 bytes)... pero eso no son 8 bytes.

Sin embargo, podéis encontrar los bytes de vuelta si se ven de la siguiente manera:

\x00 \x00 \x01 t \xD1 \x93 \x1F p

Resulta que como muestra la información por pantalla HBase es mediante arrays de caracteres que además no son "type-aware", y resulta que escapa en hexa todos los caracteres que son ASCII imprimible, y deja tal cual caracteres que no puede imprimir. Si miramos qué rango de caracteres se encuentra dentro de ASCII imprimible, vemos que son 128 caracteres (00 a 7F), pero sospecho que no transforma caracteres de 00 a 1F ya que son símbolos de control, usando realmente 20 a 7F. Básicamente, si puede pasarlo a ASCII imprimible, pasará el byte a ASCII, y si no puede, lo dejará en hexa.

Vamos a hacer el ejercicio de pasar a mano a transformar de vuelta estos bytes al long original, primero pasando a un array de bytes unsigned en decimal:

\x00 --> 00h, 000d
\x00 --> 00h, 000d
\x01 --> 01h, 001d
t    --> 74h, 116d
\xD1 --> D1h, 209d
\x93 --> 93h, 147d
\x1F --> 1Fh, 031d
p    --> 70h, 112d

Quedándonos el array siguiente:

[0,0,1,116,209,147,31,112]

Solo nos quedaría multiplicar los valores por su término correspondiente (hacer un shift de bytes):

000*2^(7*8) --> 0
000*2^(6*8) --> 0
001*2^(5*8) --> 1099511627776
116*2^(4*8) --> 498216206336
209*2^(3*8) --> 3506438144
147*2^(2*8) --> 9633792
031*2^(1*8) --> 7936
112*2^(0*8) --> 112

SUM = 0 + 0 + 1099511627776 + 498216206336 + 3506438144 + 9633792 + 7936 + 112 = 1601243914096

A los que estén relativamente puestos con tiempo Unix, verán que es un triste timestamp en ms (en concreto del 27 de noviembre de 2020).

Pero hacer esto a mano es un poco infierno. ¿Seguro que hay alguna manera de hacer esto programáticamente, verdad? Pues os traigo 2 soluciones de mi propia cosecha, 1 en JavaScript y otra en Scala, donde introduciendo el string que nos da HBase nos devuelve el querido long. Cada implementación tiene sus características (JavaScript no se lleva excesivamente bien con trabajar a nivel de byte, y hay que tirar de un ArrayBuffer, y hay que tener cuidado con que si es unsigned o signed bytes, ya que para Java sus primitivos son con signo), pero hacen bastante bien su trabajo. Os lo dejo a continuación.

JavaScript:

function toBytes(array){

    var arr = new ArrayBuffer(8);
    var view = new DataView(arr);
    for (i in array) {
        view.setUint8(i, array[i], false);
    }
    return view.getBigInt64();
}

function expandHBaseHex(input){
    if (input.length > 2){
        var p1 = input.substr(0,2);
        var p2 = input[2].charCodeAt(0);
        return [parseInt("0x"+p1), p2]
    }
    else return [parseInt("0x"+input)]

}

function HBaseHexToString(str){

    var a = str.replace(/\\x/, "").split("\\x");
    console.log(a);
    var b = a.flatMap(expandHBaseHex);
    console.log(b);
    var c = toBytes(b);
    return c;
}

HBaseHexToString("\\x00\\x00\\x01t\\xD1\\x93\\x1Fp") // escapar las barras que si no lo entiende como hexa

Scala:

import java.nio.ByteBuffer

def HBaseHexToBytes (str: String) = {
val t1 = str
.replaceFirst("^\\\\x", "")
.split("\\\\x")
.flatMap{ x =>
	if (x.length > 2) { // tiene parte de hex + parte char
		var p1 = x.substring(0,2)
		var p2 = x.apply(2).toHexString
		Seq(Integer.parseInt(p1,16),Integer.parseInt(p2,16))
		}
	else Seq(Integer.parseInt(x,16))}
t1.map(_.toByte)
}

def transformHBaseHexToLong(str: String) = ByteBuffer.wrap(HBaseHexToBytes(str)).getLong()

transformHBaseHexToLong("\\x00\\x00\\x01t\\xD1\\x93\\x1Fp")

¡Y hasta aquí el artículo de hoy! Espero que os haya sido de utilidad, y si no, pues que al menos os resulte interesante. ¡Hasta la próxima!

This post is licensed under CC BY 4.0 by the author.