You are viewing alvherre

lo que cayó, cayó [entries|archive|friends|userinfo]
alvherre

[ website | My Website ]
[ userinfo | livejournal userinfo ]
[ archive | journal archive ]

WITH TIME ZONE o WITHOUT? [Dec. 17th, 2010|05:14 pm]
[Tags|]

Muchas personas creen que el tipo TIMESTAMP WITH TIME ZONE, en PostgreSQL, almacena el huso horario, pero están equivocados. En verdad, la diferencia entre los tipos TIMESTAMP WITH TIME ZONE y TIMESTAMP WITHOUT TIME ZONE no es bien comprendida; este artículo intenta aclarar un poco el tema.

Círculo en el sol, aguacero o temblor
La diferencia entre los dos tipos es cómo se interpreta la hora. TIMESTAMP WITHOUT TIME ZONE (y además TIMESTAMP, que es un alias) asume que tú sabes perfectamente de qué TZ es la hora que le estás pasando, y que no necesitas que sea consistente con los TZs de otros valores de ese mismo tipo; o bien, que tienes 100% seguridad que siempre estarán todos en el mismo TZ. Cosa que es difícil de asegurar puesto que uno nunca tiene claridad de las líneas de desarrollo futuro de sus sistemas.

En cambio, un TIMESTAMP WITH TIME ZONE (y su alias TIMESTAMPTZ) asume que la hora está en el TZ local, configurado con la variable TimeZone (que puedes cambiar en el archivo postgresql.conf o usando la orden SET). Para almacenarla, le quita la diferencia de horas de GMT (es decir, lo “rota” a GMT) y lo guarda de esa forma; es decir, en el almacenamiento, todos los valores de una columna de tipo timestamptz van a estar en GMT. Al momento de desplegar un valor, se vuelve a rotar desde GMT hasta el TZ actual agregándole la diferencia de horas de GMT.

Si lo piensas detenidamente, te darás cuenta que esto significa que si tienes un usuario en un TZ que ingresa una hora, y luego viene otro usuario en un TZ distinto que la examina, el valor de la hora va a ser distinto para cada uno de ellos: para cada uno, va a estar en su TZ local.

Esta es una característica tremendamente útil y generalmente mal comprendida.

Entonces, lo recomendable es que almacenes el TZ que se debe aplicar en cada momento (por ej. si estás haciendo un sistema con usuarios, guardes el TZ de cada usuario) y hagas cambies el parámetro TimeZone cada vez, de manera que todas las horas del sistema sean consistentes y coherentes, sobre todo si los usuarios interactúan con otros que están en TZs distintos. Haciéndolo de esta forma te ahorras muchos dolores de cabeza en el lado de tu aplicación, puesto que te evitas tener que implementar el código para hacer las rotaciones horarias.

Mi recomendación: nunca uses los tipos TIMESTAMP WITHOUT TIME ZONE ni TIMESTAMP a menos que entiendas muy bien por qué.

Nota final: existe el operador AT TIME ZONE que permite cambiar un TIMESTAMP WITH TIME ZONE en TIMESTAMP WITHOUT TIME ZONE y viceversa.
Link1 comment|Leave a comment

Depurando punteros TOAST corruptos [Jul. 14th, 2010|12:58 pm]
[Tags|]

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$;

LinkLeave a comment

Depurando consultas lentas en PL/pgSQL [May. 13th, 2010|11:39 am]
[Tags|, , ]

Depurando consultas lentas en PL/pgSQL

Bruce Momjian and Jonah Harris
No califica como FAQ, pero una pregunta recurrente en la lista pgsql-es-ayuda es ¿por qué una consulta que al ejecutarse directamente se demora un tiempo X, pero que cuando se ejecuta en una función PL/pgSQL se demora mucho más? En un ejemplo reciente era 1000 veces más lenta en PL/pgSQL, así que es importante saber qué es lo que está pasando.

El motivo para esta lentitud tiene que ver con la ejecución con planes preparados de la consulta, en contraposición a la ejecución que usa un plan construido para esa ejecución específica.

Para ver la diferencia en forma simple, puede usarse PREPARE en la consola SQL junto con EXPLAIN EXECUTE. Reemplazando los valores constantes de la consulta con variables, y pasando los valores en EXPLAIN EXECUTE, el optimizador dejará un plan preparado que muestra el mismo efecto que se obtiene en PL/pgSQL. Si bien esto no ayuda en nada a solucionar el problema, permite visualizar el motivo e imaginar una solución.

Por ejemplo: si tenemos una función PL/pgSQL de esta forma:

CREATE FUNCTION trae_max(INT) RETURNS int AS $$
  DECLARE 
    maximo int;
  BEGIN
    ...
    SELECT INTO maximo max() FROM una_tabla WHERE id=$1;
    RETURN maximo;
  END;
$$;


Para poder ver exactamente cuál es el plan que el optimizador usará para esa consulta, usamos PREPARE de esta forma:

PREPARE una_consulta(int) AS SELECT max() FROM una_tabla WHERE id=$1;
EXPLAIN EXECUTE una_consulta(2);


Es muy posible que el plan generado de esta forma sea distinto del que se generaría si usáramos EXPLAIN directamente con la consulta:

EXPLAIN SELECT max() FROM una_tabla WHERE id=$1


En PL/pgSQL, normalmente es posible solucionar estos problemas usando EXECUTE para evitar el uso de un plan preparado. Esto forzará al motor PL/pgSQL a generar un nuevo plan para cada ejecución de la función. Más discusión al respecto puede verse en el hilo que dio origen al presente artículo.
LinkLeave a comment

Xorg: cambia mis opciones de teclado, ¡maldición! [Apr. 21st, 2010|12:18 pm]
[Tags|, , ]

 Hace unos días atrás, mi hija a manotazos me rompió la tecla Ctrl.

Tareas

(La imagen es sólo ilustrativa, porque el teclado que se rompió no es el del laptop).

No teniendo ganas de comprar un teclado nuevo por semejante pequeñez, decidí mapear Ctrl a la tecla Caps Lock (“Bloq Mayús” en un teclado español), como tantas veces había oído recomendar. “Es mucho más cómodo y natural. Además nadie usa CapsLock de todas formas”, decían. “La tecla CapsLock deberían haberla eliminado hace mucho”.

Después de buscar como 30 segundos en la web, la receta resultó muy sencilla: hay que darle la opción ctrl:nocaps a XkbOptions. En un terminal basta con ejecutar lo siguiente:

setxkbmap -option ctrl:nocaps

Todo bien, excepto que no quería estar digitando eso cada vez que abría mi sesión. Así que busqué de nuevo en la dichosa web y encontré que bastaba con agregar lo siguiente a xorg.conf:

Options "XkbOptions" "ctrl:nocaps"
en la sección InputDevice que corresponde al teclado.

Sin embargo yo ya tenía una línea con XkbOptions, con la opción "grp:alt_shift_toggle", la cual me permite cambiar fácilmente de teclado inglés a español presionando Alt y Shift, cosa que hago con mucha frecuencia. ¿Cómo se agregaba una segunda opción? Una tercera búsqueda en la web me informó que bastaba con poner ambas opciones separadas por comas, es decir

Options "XkbOptions" "grp:alt_shift_toggle,ctrl:nocaps"

Sin embargo, luego de probarlo, resultó que no funcionaba. Peor aún, no sólo no definía las opciones al abrir mi sesión, sino que además las opciones volvían al estado original cuando se abría otra sesión y retornaba a la mía. Me volví loco entonces, buscando por qué diablos podía ignorar las opciones que le indicaba. Busqué muchas veces en la omnisciente web, sin resultado … hasta que hoy encontré la respuesta por casualidad. La búsqueda me llevó a este artículo, el cual me produjo una epifanía: el gestor de teclado de Gnome estaba sobreescribiendo totalmente las opciones. De modo que abrí el “panel de control” del teclado, marqué mi opción, y listo.

A estas alturas del siglo, no entiendo por qué me sorprende que no se me haya ocurrido hurgar en las herramientas point-and-click de configuración.

Lo bueno de esta historia es que resulta que sí es bastante más cómodo usar el Caps como Ctrl, y sólo me lamento de no haberlo hecho antes. ¿Por qué no me romperían la tecla meses atrás?
LinkLeave a comment

Writer's Block: Play it again, Sam [Nov. 11th, 2009|04:03 pm]
[Tags|]

LJ::User=HASH(0x2b52fb30e000)
If you could only listen to one CD for the rest of your life, what would you choose and why?

 I would probably choose "Favourite Cello Concertos" by Jacqueline Du Pré.  It's actually a 3 CD set, but still :-)  The music in it is beautiful, powerful at times, and quiet and peaceful at others.  A perfect mixture, for all moods.  Besides, I love the cello.



LinkLeave a comment

El “OOM-killer” y “overcommit” de memoria en Linux [Sep. 29th, 2009|12:39 pm]
[Tags|, , , , ]
[Current Music |Trois Gnossiennes, Erik Satie]

Uno de los problemas relativamente comunes que aparecen en un servidor Linux que esté corriendo PostgreSQL es la aparición del “OOM-killer”, el cual puede detectarse porque en el log del kernel aparece un mensaje como el siguiente:
kernel: Out of Memory: Killed process 1337 (postmaster)

OOM significa, obviamente, “out of memory” (sin memoria), y el “OOM-killer” es una tarea del kernel que se hace cargo de una difícil situación que tiene que ver con el “overcommit de memoria”.

Overcommit (literalmente podría traducirlo como “exceso de compromiso”, pero no estoy seguro si esa frase tiene sentido en español) significa que el kernel permite más asignaciones de memoria que las que realmente tiene espacio para satisfacer. Por ej. si se tienen 2GB de RAM y 1 GB de swap, un kernel que no haga overcommit va a retornar ENOMEM ("no hay suficiente memoria") cuando haya 3 GB ya utilizados en total, y una aplicación pida más memoria. En cambio, un kernel que haga overcommit, va a retornar que sí se puede usar el espacio solicitado extra, y la aplicación puede continuar funcionando.

El problema ocurrirá cuando las aplicaciones que pidieron espacio lleguen a utilizarlo. Hay muchos casos en que esto no ocurre, por variados motivos, y es tan común que la configuración por omisión del kernel de Linux trae overcommit activado, sin mayores problemas.

Ahora, si llega a suceder que todas las solicitudes (o una mayoría de ellas) se utilicen, puede ser fatal. El problema es que cuando el kernel queda sin memoria libre y necesita satisfacer una promesa anterior de que ya se otorgó memoria a algún proceso, se ve entre la espada y la pared. La única solución que tiene para salir del paso es matar algún proceso que esté usando mucha memoria, con la esperanza de que al hacer eso se libere suficiente memoria para que el resto del sistema pueda continuar funcionando normalmente.

En un sistema sin overcommit, PostgreSQL tiene un sistema bastante bueno para protegerse de las situaciones en que se queda sin memoria: simplemente, aborta la transacción en curso y el sistema sigue funcionando normalmente. El usuario puede volver a intentar la transacción que causó el problema, y si la estrechez de memoria era transitoria, es posible que funcione.

Sin embargo, si un kernel con overcommit activo le dice a Postgres que tiene memoria disponible, y Postgres continúa procesando la transacción, pero más tarde el kernel se da cuenta que en realidad no la tenía, lo que hará el kernel será matar el proceso “postmaster”, y el servicio Postgres será terminado forzosamente, con lo cual habrá un reinicio de la base de datos: todos los proceso activos serán terminados, las conexiones serán cerradas, y el servidor tendrá que volver a levantarse. Es decir, una situación desagradable.

Por consiguiente, en un servidor PostgreSQL es importante desactivar el overcommit de memoria en el kernel. La documentación de PostgreSQL dice cómo hacerlo:
http://www.postgresql.org/docs/current/static/kernel-resources.html#AEN24162
La parte importante es poner una línea vm.overcommit_memory=2 en sysctl.conf y releer la configuración con la orden sysctl -p.
LinkLeave a comment

Nuevas versiones de PostgreSQL [Sep. 10th, 2009|11:32 am]
[Tags|, ]

Se acaba de publicar una nueva serie de versiones actualizadas de PostgreSQL para todas las ramas soportadas, desde 7.4 hasta 8.4. El anuncio en español apareció, entre otros, en postgresql.cl. (Y este es el anuncio oficial en la lista pgsql-announce). Se recomienda a todos actualizar a esta versión.

Y recuerden que para Windows sólo están soportadas las ramas 8.2, 8.3 y 8.4. En las nuevas versiones se corrigió finalmente el problema que daba lugar al error “could not reattach to shared memory”, así que los usuarios de Windows son los que estarán más contentos.

Disfruten.

LinkLeave a comment

aritmética en LaTeX [Apr. 8th, 2009|11:41 pm]
[Tags|, ]
[Current Mood |relievedrelieved]
[Current Music |Johann Sebastian Bach - Suite Orquestal nº 1 en Do Mayor, BWV 1066 - Obertura]

Hasta mis primeros años de universidad todavía estaba contagiado con el virus de WYSIWYG («what you see is what you get», que es más o menos «lo que se ve es el resultado final), es decir, usaba Office (particularmente Microsoft Word) para hacer mis documentos. Me curé en esos años cuando conocí LaTeX. Hoy en día, cuando se trata de hacer documentos, siempre uso LaTeX. En particular, llevo un par de años haciendo mis «invoices» (boletas de honorarios) en LaTeX.

Una cosa que siempre me ha molestado de estos «invoices» es que los valores son cambiantes: algunos meses tengo que restarme días de trabajo por haber estado ausente por cualquier motivo. He hecho a mano esto durante ya demasiado tiempo, y ayer decidí que era suficiente. Así que me puse a investigar cómo hacerlo un poco más sencillo.

Lo resolví con ayuda de dos personas que contestaron mi petición de ayuda en la lista de correo linux@listas.inf.utfsm.cl (muchas gracias Pablo Jiménez y Germán Poo) y un buen rato de búsqueda en el sitio de Tex Users Group tug.org. Mi solución final usa dos paquetes LaTeX que antes no conocía: el primero es fltpoint para hacer los cálculos, de esta forma:

\usepackage{fltpoint}

\fpDecimalSign{.}

\newcommand{\invoiceunitvalue}{250}
\newcommand{\invoicefactor}{0.80}

\fpMul{\invoicevalue}{\invoiceunitvalue}{\invoicefactor}


Eso me define \invoicevalue que tiene el valor de la multiplicación. Lo bueno es que ahora puedo editar dos líneas en el archivo y la tabla que es lo que realmente se imprime (y que tiene un formato de &/%$%& que prefiero no tener que editar a mano) puedo dejarla tranquila. Además, el valor \invoicevalue se usa dos veces, así que me ahorro tener que poner el número en dos lugares. (Y además me ahorro tener que abrir un terminal para ejecutar la calculadora, puesto que TeX se hace cargo de calcular).

Lo otro es que como los valores se expresan en dólares US, y los gringos usan dos decimales en sus cantidades de dinero, quiero que el número aparezca con dos decimales incluso cuando el valor es un entero. Eso lo hago con el paquete siunitx:

\usepackage{siunitx}
\sisetup{fixdp,dp=2}

\num{\invoicevalue}


A guisa de ejemplo, abajo está el código LaTeX completo de un invoice:

a sample invoice

\documentclass[12pt]{article}
\usepackage[latin1]{inputenc}
\usepackage{times}
\usepackage{fltpoint}
\usepackage{siunitx}

\fpDecimalSign{.}

\newcommand{\invoiceperiod}{March 2009}
\newcommand{\invoiceunitvalue}{500}
\newcommand{\invoicefactor}{0.8}

\fpMul{\invoicevalue}{\invoiceunitvalue}{\invoicefactor}
\sisetup{fixdp,dp=2}

\begin{document}

\pagestyle{empty}

{\LARGE Invoice for Programming Services Rendered}

\vskip 1cm

\begin{tabular}{ll}
{\bf Client} & Command Prompt, Inc. \\
{\bf Date} & \today \\
{\bf Invoice Period} & \invoiceperiod \\
\end{tabular}

{
\Large
\begin{center}
Álvaro F. Herrera \\
Mi Calle, Núm. 89 \\
Valdivia, CHILE \\
+56-63-MIFONO
\end{center}
}

\vskip 1cm

\begin{tabular}{llrrr}
{\bf Date} & {\bf Description} & {\bf Payment } & {\bf Quantity} & {\bf Amount} \\
\invoiceperiod & Outside Services & \$ \num{\invoiceunitvalue} &
\num{\invoicefactor} & \$ \num{\invoicevalue} \\
& & & & \$ \num{\invoicevalue} \\
\end{tabular}

\vskip 1cm
{\large Please make payment via SWIFT to:}

\begin{tabular}{ll}
{\bf Beneficiary} & Álvaro HERRERA Muñoz \\
{\bf Beneficiary's Account} & Número de cuenta \\
{\bf Bank of Beneficiary} & Nombre del banco \\
{\bf Bank Address} & Dirección del banco \\
 & Valdivia, CHILE \\
{\bf SWIFT code} & Código SWIFT del banco \\
\end{tabular}

\end{document}

LinkLeave a comment

Ni muy caliente que se derrita el helado ... [Apr. 21st, 2006|01:18 pm]
Ni muy frío que congele la lechuga.

Cuando recién llegamos a este departamento, el refrigerador no quedó funcionando muy bien. No enfriaba la puerta de abajo (esa que supuestamente debe estar a unos 5ºC), pero al principio el problema era sutil así que lo dejamos pasar. Con los días fue empeorando, de manera que el lunes de la semana pasada llamamos al servicio técnico para que le echaran un vistazo.

El miércoles vino el técnico. Lo desarmó entero; le sacó y probó los sensores; verificó que la bomba y demás aparataje estaba funcionando bien; el "líquido" (o es un gas?) está en buen estado y no hay fugas; etc. Nada visible. Así que finalmente le sacó una placa con un circuito electrónico y algo que parecía un microcontrolador, lo miró por todos lados y me dijo: "esta memoria es lo que está fallando".

Me pareció divertido que lo llamara "memoria", pero me pareció mucho menos divertido cuando me dijo que tenía un plazo de cinco días hábiles para generar un presupuesto; que la aseguradora tenía un plazo de tres días hábiles a partir de ahí para aprobar el presupuesto; que tenía no se cuántos días más para comprarlo (aparentemente puede ser necesario importarlo), y de ahí hasta que lo instalen anda tu a saber cuántos días hábiles más iban a pasar hasta que tuvieramos funcionando nuestro inhábil refrigerador.

No quedé muy contento, pero el tipo armó todo y se fue dejándome una orden de atención que muestra en la cabecera de página los logos de muchas marcas de refrigeradores, excepto aquel de la de nuestro refrigerador.

Pasó un par de horas y de pronto me di cuenta que llevaba algún tiempo escuchando un ruido extraño en el refrigerador. ¡Vaya! Se había puesto a funcionar por algún motivo. Quizás el tipo había apretado algún contacto sin darse cuenta, o había limpiado algo, o había destapado un sensor. Cosas de la vida. El asunto es que ese día viajábamos al norte así que dejé el asunto ahí.

Hoy descubrimos que el refrigerador no sólo está funcionando, sino que aparentemente tiene algún delirio de megalomanía, porque enfría mucho más de lo esperado: había lechuga en una caja y se congeló.

En fin, harto temperamental el bicho.
Link1 comment|Leave a comment

(no subject) [Dec. 28th, 2001|09:26 pm]
Como cada vez que tenemos que viajar, nos demoramos mucho más en levantarnos que lo normal. Con toda la calma del mundo tomamos desayuno, algunos se fueron a meter a la cocha y después de mucho rato por fin empezamos a empacar.

Cuando tuvimos la camioneta con todos nuestros bártulos listos, emprendimos el vuelo iniciando el éxodo final hacia el sur. Primera parada: Antofagasta.

El viaje no tuvo nada de particular. Llegamos como a las siete, y sintiéndonos en terreno de metrópolis lo primero que hicimos fue meternos en el Líder. Compramos algunos artículos de primerísima necesidad y partimos a buscar un lugar apropiado para acampar. Dada la época del año y sobre todo el escasísimo interés turístico de la ciudad, obviamente no encontramos nada decente. A decir verdad, a veinte kilómetros de la ciudad hay tres o cuatro campings establecidos; todos, excepto uno, estaban cerrados. El que hubiera uno abierto fue, creo yo, más una expresión de nuestra mala suerte que otra cosa.
LinkLeave a comment

navigation
[ viewing | most recent entries ]
[ go | earlier ]