alvherre (alvherre) wrote,

Depurando punteros TOAST corruptos

Es frecuente encontrarse con el problema de que un puntero TOAST se ha corrompido en la base de datos. Normalmente esto puede verse al encontrar una consulta que falla con un mensaje de error similar a este:

ERROR:  invalid memory alloc request size 1818585462


Depurar estos problemas no es fácil, porque hay que encontrar la ubicación del dato que está corrupto para poder tomar alguna acción con él. Típicamente se le pide al usuario que haga búsqueda binaria sobre la tabla, usando las claúsulas LIMIT y OFFSET de manera de restringir el conjunto de valores hasta encontrar exactamente qué registro o registros están corruptos. Este método es muy lento.

Para resolver este problema, Enova Financial pidió a Command Prompt que desarrollara un mecanismo para encontrar fácil y rápidamente aquellos datos que presentaban algún nivel de corrupción. El resultado es la función que está al final de este artículo.

Muy a grandes rasgos, lo que hace la función de abajo es, dado el nombre de una tabla, escribir otra función que recorre dicha tabla y reporta el CTID (posición física) y los valores de llave primaria de todos aquellos registros que fallan un control de sanidad básico.

Una vez que el o los registros han sido identificados, es cuestión de determinar qué se quiere hacer con el valor corrupto. Una posibilidad es hacer un UPDATE del valor de esa columna a NULL. Otra posibilidad es hacer un UPDATE del valor correcto (cosa que puede ser difícil de determinar); una tercera sería eliminar (DELETE) el registro completamente. Aún una cuarta sería intentar depurar más finamente el sistema para encontrar cuál es el valor correcto que debería haber. Esto último queda de ejercicio para el lector.

A continuación, el código de la meta-función:

create or replace function toastcheck_writer(text) returns void language plpgsql as $ff$
  declare
    func text;
    funcname text;
    column record;
    pkc    record;
    indent text;
    colrec record;
    pkcols text;
    pkformat text;
    pk_col_ary text[];
  begin

  pkcols = '';
  pkformat = '';
  pk_col_ary = '{}';
  funcname = 'toastcheck__' || $1;

  FOR pkc IN EXECUTE $f$ SELECT attname
                           FROM pg_attribute JOIN
                                pg_class ON (oid = attrelid) JOIN
                                pg_index on (pg_class.oid = pg_index.indrelid and attnum = any (indkey))
                          WHERE pg_class.oid = '$f$ || $1 || $f$ '::regclass and indisprimary $f$
  LOOP
     IF pkcols = '' THEN
        pkcols = quote_ident(pkc.attname);
        pkformat = '%';
     ELSE
        pkcols = pkcols || ', ' || quote_ident(pkc.attname);
        pkformat = pkformat || ', %';
     END IF;
     pk_col_ary = array_append(pk_col_ary, quote_ident(pkc.attname));
  END LOOP;

  /*
   * This is the function header.  It's basically a constant string, with the
   * table name replaced a couple of times and the primary key columns replaced
   * once.  Make sure we don't fail if there's no primary key.
   */
  IF pkcols <> '' THEN
     pkcols = ', ' || pkcols;
     pkformat = ', PK=( ' || pkformat || ' )';
  END IF;
  func = $f$
    CREATE OR REPLACE FUNCTION $f$ || funcname || $f$() RETURNS void LANGUAGE plpgsql AS $$
     DECLARE
       rec record;
     BEGIN
     FOR rec IN SELECT ctid $f$ || pkcols || $f$ FROM $f$ || $1 || $f$ LOOP
        DECLARE
          f record;
          l int;
        BEGIN
          SELECT * INTO f FROM $f$ || $1 || $f$ WHERE ctid = rec.ctid;

          -- make sure each column is detoasted and reported separately
$f$;

   /* We now need one exception block per toastable column */
   indent = '          ';
   FOR column in SELECT attname
                 FROM pg_attribute JOIN pg_class on (oid=attrelid)
                 WHERE pg_class.oid = $1::regclass and attlen = -1
   LOOP
      func := func || indent || E'BEGIN\n';
      func := func || indent || $f$  SELECT length(f.$f$ ||
              quote_ident(column.attname) || E') INTO l;\n';

     /* The interesting part here needs some replacement of the PK columns */
     func := func || indent || $f$EXCEPTION WHEN OTHERS THEN
	    RAISE NOTICE 'TID %$f$ || pkformat || $f$, column "$f$ || column.attname || $f$": exception {{%}}',
			     rec.ctid, $f$;

     /* This iterates zero times if there are no PK columns */
     FOR colrec IN SELECT f.i[a] AS pknm
		  FROM (select pk_col_ary as i) as f,
		       generate_series(array_lower(pk_col_ary, 1), array_upper(pk_col_ary, 1)) as a
     LOOP
       func := func || $f$ rec.$f$ || colrec.pknm || $f$, $f$;
     END LOOP;

     func := func || E'sqlerrm;\n';
     func := func || indent || E'END;\n';
   
   END LOOP;

   /* And this is our constant footer */
   func := func || $f$ 
       END;
     END LOOP;
     END;
    $$;
  $f$;

  EXECUTE func;
  RAISE NOTICE $f$Successfully created function %()$f$, funcname;
  RETURN;
  END;
$ff$;

Tags: postgresql-es
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic
  • 0 comments