Conocimientos básicos del control de exclusión en la integración de archivos — Mejores prácticas de bloqueo de archivos y claim atómico

· Actualizado el: · · Integración de archivos, Control de exclusión, Diseño, Desarrollo en Windows

El control de exclusión en la integración de archivos casi siempre se convierte en un problema cuando hay carpetas compartidas, procesos batch nocturnos o integración entre procesos distintos. Entre las búsquedas más frecuentes destacan preguntas como si basta con el bloqueo de archivos, cómo evitar que varios workers tomen el mismo archivo, o cómo evitar leer un archivo que todavía se está escribiendo.

En este artículo repasamos el control de exclusión en la integración de archivos tomando como ejes el bloqueo de archivos, el claim atómico, temp -> rename e idempotency.

Alinear la terminología primero

En este campo, muchos términos se han asentado tal cual en inglés, y si su significado queda difuso resulta difícil de leer. Antes de continuar, fijamos aquí el sentido que les damos en este artículo.

Término Significado en este artículo
Atómico (atomic) Una operación cuyo estado intermedio no es visible desde fuera. Solo puede terminar en éxito o en que no haya pasado nada
claim Asegurar el derecho de procesamiento diciendo “este archivo lo proceso yo”. En este artículo se refiere principalmente a que solo se convierte en propietario el lado que logra el rename de incoming a processing/<worker>/
claim atómico Realizar ese claim en una sola operación. Si la confirmación y la reserva están separadas, otro proceso puede colarse en el intervalo (3.1)
lease Propiedad con fecha de caducidad. En el lock file se anota “quién” y “hasta cuándo”, de modo que, al expirar el plazo, otro worker pueda tomar el relevo (4.4)
stale El estado de un lock o claim que queda abandonado porque su dueño terminó de forma anómala. Si no se puede determinar si sigue vivo o no, todos se detienen (2.3)
manifest Un archivo de descripción del contenido, separado del archivo principal. En él se anota el nombre de archivo, el tamaño, el hash, el número de registros, etc., y se usa para la verificación del lado receptor. El archivo done es su versión mínima (4.2)
idempotency (idempotencia) La propiedad de que, aunque se procese la misma entrada otra vez, el resultado no cambia (4.5)
advisory lock Un bloqueo que solo funciona si todos los participantes respetan esa convención. Como el sistema operativo no lo impone, se puede escribir sin problema un programa que lo ignore y lea o escriba igualmente. El flock de Linux es de este tipo
byte-range lock Un bloqueo que no afecta a todo el archivo, sino solo al rango indicado. El caso representativo es LockFileEx de Windows, que sí es impuesto por el sistema operativo. Sin embargo, tiene una excepción (3.5)

Índice

  1. Primero, la conclusión (en una frase)
  2. Patrones de conflicto que ocurren en la integración de archivos (con diagramas)
    • 2.1. Leer un archivo que todavía se está escribiendo
    • 2.2. Varios workers toman el mismo archivo a la vez
    • 2.3. Un stale lock detiene a todos
  3. Antipatrones
    • 3.1. La comprobación en dos pasos Exists -> Create
    • 3.2. Escribir directamente en el nombre de archivo final
    • 3.3. Dar por completado cuando el tamaño del archivo deja de cambiar
    • 3.4. Actualizar un archivo compartido entre todos
    • 3.5. Pensar que la API de bloqueo lo resuelve todo
  4. Mejores prácticas
    • 4.1. Publicar con temp -> close -> rename / replace
    • 4.2. Explicitar la integridad con done / manifest
    • 4.3. El lado receptor toma el claim de forma atómica
    • 4.4. Si se depende de un lock file, convertirlo en lease
    • 4.5. Dar por sentada la idempotency
  5. Pseudocódigo (extracto)
  6. Guía rápida de uso según el caso
  7. Resumen
  8. Referencias

En la integración de archivos, lo que suele romperse no es tanto el código en sí como la “convención de entrega”. Pasa las pruebas unitarias sin problema, pero de vez en cuando falla solo en la carpeta compartida de producción o en el batch nocturno. Y encima es difícil de reproducir. Es un caso bastante habitual.

En muchos casos la causa no está en la propia API de E/S de archivos, sino en que estos tres puntos quedan ambiguos.

  • Cuándo está permitido leer
  • Quién tiene el derecho de procesamiento
  • Cómo recuperarse cuando algo falla

En este artículo no dejamos el control de exclusión en la integración de archivos reducido a los bloqueos del sistema operativo, sino que lo organizamos como un protocolo de entrega.

El código que aparece en este artículo se publica en GitHub como un conjunto de ejemplos que se pueden compilar y ejecutar (una librería, una demo que muestra la competencia por el claim entre dos workers y la toma de un lease, y pruebas unitarias que reproducen conflictos, corrupción y stale locks).

file-integration-locking-best-practices-komurasoft-style - komurasoft-blog-samples (GitHub)

1. Primero, la conclusión (en una frase)

  • Lo más importante en la integración de archivos es que, en el momento en que el nombre de archivo final se vuelve visible, ya esté en un estado de “ya se puede leer”
  • Representar los estados en generación / publicado / en proceso / procesado mediante el nombre de archivo o el directorio
  • Si hay varios workers, tomar el claim de forma atómica antes de leer
  • Usar el lock file o el bloqueo del sistema operativo solo como apoyo, y confiar finalmente en la idempotency

En definitiva, en la integración de archivos lo central no es tanto el control de exclusión como el diseño del protocolo de entrega. No basta con llamar a una función de bloqueo y darlo por terminado.

2. Patrones de conflicto que ocurren en la integración de archivos (con diagramas)

2.1. Leer un archivo que todavía se está escribiendo

Este incidente ocurre cuando se empieza a escribir directamente en el nombre de archivo final. Si es JSON, falta el corchete de cierre; si es CSV, faltan filas; si es ZIP, simplemente queda corrupto.

Lectura de un archivo mientras todavía se está escribiendoDiagrama de secuencia que muestra cómo el lado receptor detecta orders.csv por su nombre final y empieza a leerlo mientras el lado emisor todavía está escribiendo el resto de las filas, provocando una lectura incompleta"Lado receptor""Carpeta compartida""Lado emisor""Lado receptor""Carpeta compartida""Lado emisor"todavía está a mediasfilas insuficientes / fallo de análisis / procesado solo en partecrea orders.csv con el nombre finalestá escribiendo las filas 1 a 5000detecta orders.csvempieza a leerlo tal cualescribe el resto

2.2. Varios workers toman el mismo archivo a la vez

Con un flujo de “mirar la lista y, si no está procesado, abrirlo”, dos workers pueden tomar el mismo archivo. Es el principio de un doble conteo o un doble envío.

Dos workers tomando el mismo archivo a la vezDiagrama de secuencia que muestra cómo dos workers encuentran el mismo a.csv en incoming y ambos empiezan a leerlo, procesando la misma entrada por duplicadoincoming"Worker 2""Worker 1"incoming"Worker 2""Worker 1"procesa la misma entrada por duplicadoencuentra a.csvencuentra a.csvempieza a leerempieza a leer

2.3. Un stale lock detiene a todos

Un diseño que se limita a colocar un lock file tiende a atascarse cuando hay una terminación anómala. Si no se sabe de quién es el lock, si sigue vivo o hasta cuándo es válido, el resto queda esperando para siempre.

Un stale lock detiene a todos los workersDiagrama de secuencia que muestra cómo el worker A crea un lock y termina de forma anómala, y el worker B, al no poder determinar si el lock sigue vivo, pospone el inicio del procesamiento y queda esperando indefinidamente"Worker B""archivo lock""Worker A""Worker B""archivo lock""Worker A"aquí termina de forma anómalano se puede determinar si es stale y todos quedan detenidoscrea el lockcomprueba si el lock existepospone el inicio del procesamientosigue esperando

3. Antipatrones

3.1. La comprobación en dos pasos Exists -> Create

El problema aquí es que “comprobar” y “reservar” son operaciones separadas. Como otro proceso puede colarse en el intervalo, esto no logra la exclusión.

La comprobación en dos pasos Exists -> Create no es exclusiónDiagrama de secuencia que muestra cómo los procesos A y B comprueban por separado que no existe el lock, ambos reciben la respuesta de que no existe, y los dos terminan creando el lock y avanzando a la vez"Sistema de archivos""Proceso B""Proceso A""Sistema de archivos""Proceso B""Proceso A"ambos terminan avanzandocomprueba si no existe el lockcomprueba si no existe el lockno existeno existecrea el lockcrea el lock

Un ejemplo típico y problemático tiene esta forma.

if (!File.Exists(lockPath))
{
    File.WriteAllText(lockPath, Environment.ProcessId.ToString());
    ProcessFile();
}

Lo que se necesita es convertir “crear si no existe” en una sola operación. En .NET se puede usar la familia FileMode.CreateNew, y en sistemas POSIX una creación atómica como O_CREAT | O_EXCL.

3.2. Escribir directamente en el nombre de archivo final

Si el lado receptor interpreta que “en cuanto ese nombre es visible, ya se puede leer”, empezar a escribir directamente en el nombre de archivo final ya es una derrota. La base es no confundir que el archivo sea visible con que ya se pueda leer.

El nombre final visible no significa que el contenido esté completoDiagrama de flujo que muestra cómo, en cuanto el nombre final se vuelve visible, el lado receptor lo detecta mientras el lado emisor todavía está escribiendo, dando lugar a la lectura de datos incompletosEl nombre final se vuelve visibleEl lado receptor lo detectaEl lado emisor todavía está escribiendoSe leen datos incompletos
using var writer = OpenForWrite(finalPath); // aquí finalPath ya se vuelve visible
foreach (var row in rows)
{
    writer.WriteLine(row);
}

Esta forma de trabajar termina provocando el incidente de 2.1 por cuenta propia.

3.3. Dar por completado cuando el tamaño del archivo deja de cambiar

Esto parece cómodo, pero es bastante peligroso. Con copias a través de la red, pausas del lado emisor, buffering o reintentos, el tamaño fluctúa con normalidad.

Dar por completado un archivo cuyo tamaño solo dejó de cambiar temporalmenteDiagrama de secuencia que muestra cómo el lado emisor copia data.zip y se pausa a medio camino, el lado receptor interpreta que el tamaño sin cambios durante 10 segundos significa que terminó y empieza a leer, justo cuando la copia se reanuda"Lado receptor""Carpeta compartida""Lado emisor""Lado receptor""Carpeta compartida""Lado emisor"lo interpreta erróneamente como completadoempieza a copiar data.zipse pausa a medio caminoel tamaño no cambia en 10 segundosempieza a leerreanuda la copia
if (currentLength == lastLength && stableSeconds >= 10)
{
    return Ready;
}

Si se decide la finalización por conjetura, en carpetas compartidas o archivos grandes esto termina jugando en contra. Es más estable explicitar la finalización mediante un manifest o un done file.

3.4. Actualizar un archivo compartido entre todos

Un diseño en el que todos leen y actualizan un mismo status.csv o counter.json termina, casi siempre, ganando el que escribe último. Cuando se empieza a usar la integración de archivos como una base de datos simplificada, aquí es donde se sufre.

Dos batches actualizando el mismo archivo compartidoDiagrama de secuencia que muestra cómo los batches A y B leen la misma versión v1 de status.csv y luego escriben cada uno su propia versión, de modo que la actualización de A se pierde bajo la de B"status.csv""Batch B""Batch A""status.csv""Batch B""Batch A"la actualización de A se pierdelee v1lee v1escribe v2-Aescribe v2-B

También existe la opción de escapar hacia un esquema append-only, pero su significado varía según el sistema de archivos y la forma de despliegue. Si se necesita una actualización compartida, es mejor no forzar aquí la integración de archivos.

3.5. Pensar que la API de bloqueo lo resuelve todo

La API de bloqueo es importante, pero solo funciona cuando todos los participantes operan bajo la misma convención. En la integración entre sistemas heterogéneos, es más seguro no confiar demasiado en esto.

Nota complementaria:

  • El flock de Linux es un advisory lock, así que se puede escribir sin problema una contraparte que ignore la convención
  • El byte-range lock de Windows se ignora en los archivos mapeados en memoria
  • En otras palabras, es mejor no hacer que el bloqueo del sistema operativo por sí solo cargue con el diseño de la notificación de finalización o de la propiedad

El segundo punto está documentado explícitamente como una especificación de Windows. En Locking and Unlocking Byte Ranges in Files de Microsoft Learn se indica que, si otro proceso accede a un rango ya bloqueado, siempre falla (es decir, el range lock de Windows se impone y no es advisory), y justo después se incluye la advertencia de que al usar archivos mapeados en memoria, el byte-range lock se ignora. Es decir, si la contraparte toca el mismo archivo a través de CreateFileMapping, nuestro bloqueo simplemente se pasa por alto.

Si se quiere tomar un range lock en .NET, se usa FileStream.Lock / Unlock (en el caso de Windows).

using var stream = new FileStream(
    path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);

// bloquear en exclusiva solo el primer byte como marca de "en proceso"
stream.Lock(0, 1);
try
{
    // aquí se hace la lectura/escritura del contenido principal
}
finally
{
    // liberar siempre antes de cerrar
    stream.Unlock(0, 1);
}

Esta forma es válida entre aplicaciones que operan bajo la misma convención. Pero, como se ha visto, no funciona si la contraparte pasa por un mapeo en memoria, y tampoco hay garantía de que otros sistemas vayan a respetar esta marca. Por eso el protocolo de entrega del capítulo 4 es lo que realmente sostiene el diseño.

4. Mejores prácticas

Antes de nada, dejamos aquí la correspondencia con los antipatrones del capítulo 3. Si tiene la sensación de estar incurriendo en alguno de ellos, también puede empezar directamente por la sección correspondiente.

Antipatrón Qué ocurre Contramedida correspondiente
3.1. La comprobación en dos pasos Exists -> Create Otro proceso se cuela en el intervalo entre comprobar y reservar, y dos procesos avanzan a la vez 4.3 Tomar el claim de forma atómica (rename o FileMode.CreateNew)
3.2. Escribir directamente en el nombre de archivo final El lado receptor lee un archivo que todavía se está escribiendo 4.1 Publicar con temp -> close -> rename / replace
3.3. Dar por completado cuando el tamaño del archivo deja de cambiar Se interpreta erróneamente una pausa de la copia como finalización 4.2 Explicitar la finalización con done / manifest
3.4. Actualizar un archivo compartido entre todos El lado que escribe después sobrescribe al anterior y la actualización desaparece Limitar el escritor a uno solo en 4.3 y absorber el doble procesamiento en 4.5. Si aun así no basta, la decisión de retirarse del capítulo 6
3.5. Pensar que la API de bloqueo lo resuelve todo Se rompe con una contraparte que no respeta la convención o que pasa por un mapeo en memoria Recibirlo con el lock file como lease en 4.4 y con idempotency en 4.5

4.1. Publicar con temp -> close -> rename / replace

Es el camino estándar. El archivo en generación se confina bajo un nombre temp y, después de hacer close, se cambia al nombre final. El lado receptor solo debe mirar el nombre final.

Publicación mediante temp -> close -> rename / replaceDiagrama de flujo que muestra el proceso de crear un nombre temp único, escribir todo el contenido en él, hacer flush y close, renombrarlo o reemplazarlo por el nombre final en el mismo directorio, y hacer que el lado receptor solo vigile el nombre finalCrear un nombre temp únicoEscribir todo el contenido en tempHacer flush / closeRename / replace al nombre final en el mismo directorioEl lado receptor solo vigila el nombre final

Puntos clave:

  • Colocar temp y final en el mismo directorio, o al menos en el mismo volumen / sistema de archivos
  • En Windows / .NET se puede considerar la familia File.Replace
  • Establecer como convención que, en cuanto el nombre final es visible, el contenido ya está completo

Si temp se coloca en otra unidad, el rename termina equivaliendo a una simple copia, o Replace puede fallar. Esta premisa es poco vistosa, pero muy importante.

A través de una carpeta compartida (SMB), además, estos 4 puntos se vuelven inestables. Precisamente ese es el terreno central de este artículo, así que los detallamos por separado.

  • El rename dentro del mismo directorio del mismo recurso compartido se ejecuta en el lado del servidor. Por lo tanto, la propiedad de que “el nombre intermedio no se ve” se mantiene tal cual. Por el contrario, si se cruza entre recursos compartidos, como de \\server\shareA a \\server\shareB, se trata como un volumen distinto, y el MoveFileEx de Windows, cuando se especifica MOVEFILE_COPY_ALLOWED, sustituye el movimiento por una copia más una eliminación. Es decir, deja de ser atómico y se vuelve visible el estado intermedio. La premisa de colocar temp y final, así como incoming y processing, dentro del mismo recurso compartido es todavía más importante que en el caso local
  • El rename falla con solo que “alguien lo tenga abierto”. A las carpetas compartidas acceden actores que no controlamos, como el antivirus, indexadores de búsqueda o clientes de otras sedes. Es realista tratar el fallo del rename de publish y de claim no como una anomalía, sino como una ramificación normal, e implementar un reintento con una espera breve de por medio
  • La marca de tiempo no sirve como criterio de decisión. En File Times de Microsoft Learn se indica que, respecto a los tiempos de archivo, lo único garantizado es que “se reflejan correctamente en el momento en que se cierra el handle que hizo la modificación”. Mientras se está escribiendo, la fecha de última modificación no se actualiza por completo hasta que se cierran todos los handles de escritura. La granularidad también depende del sistema de archivos: la fecha de última modificación en FAT tiene una resolución de 2 segundos, y la fecha de último acceso en NTFS puede actualizarse con hasta 1 hora de retraso. Además, a través de SMB la marca de tiempo la pone el reloj del servidor, así que si el reloj del cliente y el del servidor están desincronizados, un criterio del tipo “procesar cuando hayan pasado N minutos desde la actualización” también queda desviado. De ahí que la finalización no se determine por la hora o el tamaño, sino con done / manifest, como se explica en 4.2
  • Las notificaciones de cambio también se pierden. Es más estable no depender únicamente de las notificaciones de eventos para vigilar la carpeta compartida, y combinarlas con un listado periódico del directorio. Este tema está resumido en Guía práctica de FileSystemWatcher: pérdidas de eventos y duplicados

4.2. Explicitar la integridad con done / manifest

Si, además de los datos principales, se explicita “qué quedó completo” en un archivo aparte, el lado receptor gana estabilidad. Esto es especialmente útil en la integración entre sistemas heterogéneos.

Explicitar la integridad con done / manifestDiagrama de flujo que muestra la generación de data.tmp, su publicación como data.csv, la creación de data.done o manifest.json, la detección de ese archivo por el lado receptor y la verificación del nombre de archivo, el tamaño y el hashGenerar data.tmpPublicar como data.csvCrear data.done / manifest.jsonEl lado receptor detecta done / manifestVerificar nombre de archivo, tamaño y hash

Los elementos que conviene incluir en el manifest son, aproximadamente, estos.

  • Nombre del archivo de destino
  • Tamaño
  • Hash
  • Número de registros
  • ID de integración / idempotency key
  • Hora de generación

El orden también importa. Si se coloca done antes de publicar el archivo principal, deja de ser una notificación de finalización y se convierte en el anuncio de un incidente.

4.3. El lado receptor toma el claim de forma atómica

Si varios workers observan el mismo incoming, resulta más claro “moverlo a lo propio antes de leerlo”. Solo procesa el worker cuyo rename de incoming a processing/<worker>/ tuvo éxito.

El lado receptor toma el claim mediante rename atómicoDiagrama de secuencia que muestra cómo dos workers encuentran a.csv en incoming e intentan renombrarlo hacia processing, y solo el que logra el rename primero se convierte en propietarioprocessingincoming"Worker 2""Worker 1"processingincoming"Worker 2""Worker 1"solo el que tuvo éxito primero toma la propiedadencuentra a.csvencuentra a.csvrenombra a.csvrenombra a.csv

En la operación, separar también los directorios facilita el seguimiento.

Separación de directorios en temp, incoming, processing, archive y errorDiagrama de flujo que muestra el recorrido de un archivo desde temp, publicado en incoming, reclamado en processing, y movido a archive si tiene éxito o a error si fallapublishclaiméxitofallotempincomingprocessingarchiveerror

El rename usado para el claim también parte de la premisa de realizarse dentro del mismo sistema de archivos.

4.4. Si se depende de un lock file, convertirlo en lease

Si se usa un lock file, en lugar de un simple archivo vacío, debe ser información de propiedad con fecha de caducidad. Un lock del que no se sabe quién lo tomó termina causando problemas más adelante, sin excepción.

Campos de un lock file convertido en leaseDiagrama de flujo que muestra los seis campos que componen lock.json como lease, ownerId, host, pid, acquiredAt, expiresAt y heartbeatAtlock.jsonownerIdhostpidacquiredAtexpiresAtheartbeatAt

Puntos clave:

  • Realizar la creación de forma atómica
  • Usar la interrupción de las actualizaciones como criterio para determinar si es stale
  • La eliminación, en principio, solo la realiza quien lo creó
  • Definir de antemano un procedimiento de recuperación, asumiendo que puede quedar sin liberar

El lock file es, en definitiva, una marca para la coordinación. Intentar garantizar con ella sola una consistencia completa suele resultar difícil.

4.5. Dar por sentada la idempotency

El control de exclusión es importante, pero en la operación real no se puede reducir a cero que “a veces llegue duplicado” o que “se reejecute a medio camino”. Al final, lo que funciona es un diseño que no se rompe aunque vuelva a procesar la misma entrada.

Diseño de procesamiento basado en idempotencyDiagrama de flujo que muestra cómo, dada una entrada con su idempotency key, si ya fue procesada se trata como éxito sin ejecutar de nuevo, y si no fue procesada se ejecuta el procesamiento y se registra en el libro de procesadosnoEntrada + idempotency key¿Ya se procesó?Se trata como éxito sin reejecutarSe ejecuta el procesamientoSe registra en el libro de procesados

Por ejemplo, se puede asignar un ID de integración a cada archivo recibido y registrarlo en un libro de procesados. Incluso si la exclusión se rompe una vez, dejarlo preparado para que el resultado no se contabilice por duplicado facilita mucho la operación.

5. Pseudocódigo (extracto)

Los nombres MakeTempPathSameDirectory o TryClaimBundleByRename que aparecen a partir de aquí son nombres de función ficticios puestos solo para mostrar el orden. La implementación que realmente funciona está en el conjunto de ejemplos presentado al principio.

Implementación de este pseudocódigo (librería, demo de competencia por el claim entre dos workers, pruebas unitarias) - komurasoft-blog-samples (GitHub)

5.1. Patrón de fallo típico

var lockPath = finalPath + ".lock";

if (!File.Exists(lockPath))
{
    File.WriteAllText(lockPath, "");
    using var writer = OpenForWrite(finalPath); // escribe directamente en el nombre final
    WritePayload(writer);

    File.Delete(lockPath);
}

Hay tres problemas.

  • Exists y WriteAllText son operaciones separadas
  • finalPath se vuelve visible desde mitad de la escritura
  • En caso de terminación anómala, el lock queda abandonado

5.2. Un ejemplo en la dirección correcta (dicho de forma resumida, sería así)

var tempPath = MakeTempPathSameDirectory(finalPath);
WritePayload(tempPath);
FlushAndClose(tempPath);

PublishByRenameOrReplace(tempPath, finalPath); // se asume mismo FS / mismo volume
PublishDoneFile(finalPath + ".done", new
{
    FileName = Path.GetFileName(finalPath),
    Size = GetFileSize(finalPath),
    Hash = ComputeHash(finalPath),
    IdempotencyKey = integrationId
});
if (!TryClaimBundleByRename(baseName, incomingDir, processingDir))
{
    return; // otro worker ya lo tomó primero
}

var manifest = ReadDoneFile(Path.Combine(processingDir, baseName + ".done"));
VerifyPayload(Path.Combine(processingDir, baseName), manifest);

if (AlreadyProcessed(manifest.IdempotencyKey))
{
    MoveBundle(processingDir, archiveDir, baseName);
    return;
}

Process(Path.Combine(processingDir, baseName));
RecordProcessed(manifest.IdempotencyKey);
MoveBundle(processingDir, archiveDir, baseName);

Aquí lo importante es el orden más que los detalles de implementación. No mezclar “escribir”, “publicar”, “tomar la propiedad” y “registrar como procesado” hace que sea menos propenso a romperse.

6. Guía rápida de uso según el caso

  • Con un único writer, un único reader y el mismo host, ya solo con temp -> rename se logra bastante estabilidad
  • Si hay varios consumers, hay que introducir el claim rename de incoming -> processing
  • Para integración entre sistemas heterogéneos, NAS o carpetas compartidas, es más seguro llegar hasta manifest / done e idempotency
  • Si varios writers quieren actualizar el mismo estado lógico, conviene no forzar demasiado la integración de archivos y considerar también una base de datos o una cola
  • El bloqueo del sistema operativo es válido dentro del mismo grupo de aplicaciones y bajo la misma premisa, pero no sustituye al protocolo de entrega

El último punto también es una decisión de retirada. De verdad existen problemas que resultan dolorosos si se abordan con archivos.

7. Resumen

El control de exclusión en la integración de archivos no consiste en llamar a una función de bloqueo, sino en decidir la transición de estados. Ese es el eje central de este artículo. Se representan los estados en generación / publicado / en proceso / procesado mediante nombres o directorios, y se evita la comprobación en dos pasos Exists -> Create, la escritura directa en el nombre de archivo final, la espera de estabilización del tamaño, la actualización mutua de archivos compartidos y el exceso de confianza en la API de bloqueo. Sobre esa base, si se combinan temp -> close -> rename / replace, done / manifest, el claim rename, el lease y la idempotency, se pueden evitar en buena medida los incidentes en la integración por carpetas compartidas.

En la integración de archivos, el truco está en no confundir “poder leer” con “estar permitido leer”. Con solo separar estos dos conceptos, se reducen notablemente los incidentes del tipo que solo aparecen de madrugada.

8. Referencias

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Basta con la API de bloqueo para el control de exclusión en la integración de archivos?
En muchos casos no basta. El flock de Linux es un advisory lock, así que se puede escribir sin problema una contraparte que ignore esa convención, y el byte-range lock de Windows se ignora en archivos mapeados en memoria. El bloqueo del sistema operativo es válido dentro de un mismo grupo de aplicaciones y bajo las mismas premisas, pero conviene usarlo como apoyo y convertir en el núcleo el diseño del protocolo de entrega: temp -> rename, done/manifest, claim atómico e idempotency.
¿Cómo evito que se lea un archivo mientras todavía se está escribiendo?
El camino estándar es publicarlo mediante temp -> close -> rename/replace. El archivo en generación se confina bajo un nombre temp, y después de hacer close se cambia al nombre final dentro del mismo directorio; el lado receptor solo debe mirar el nombre final. Se parte de la premisa de que temp y final están en el mismo directorio, o al menos en el mismo volumen/sistema de archivos, y se establece como convención que, en cuanto el nombre final es visible, el contenido ya está completo.
¿Cómo evito que varios workers procesen el mismo archivo a la vez?
Se toma el claim de forma atómica antes de leer. En concreto, solo procesa el worker cuyo rename de incoming a processing/<worker>/ tuvo éxito. La comprobación en dos pasos Exists -> Create no logra la exclusión, porque 'comprobar' y 'reservar' son operaciones separadas y otro proceso puede colarse entre ambas. Si se necesita una creación atómica, se usa la familia FileMode.CreateNew de .NET o O_CREAT | O_EXCL de POSIX.
¿Hay algo que tener en cuenta al usar un lock file?
En lugar de un simple archivo vacío, conviene convertirlo en un lease (información de propiedad) con fecha de caducidad, que incluya ownerId, host, pid, acquiredAt, expiresAt y heartbeatAt. La creación se realiza de forma atómica, la interrupción de las actualizaciones se usa como criterio para determinar si es stale, la eliminación la realiza en principio solo quien lo creó, y se define de antemano un procedimiento de recuperación asumiendo que puede quedar sin liberar. En la práctica funciona bien no intentar garantizar la consistencia completa con un solo lock file, sino recibirlo finalmente con idempotency.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog