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: · Go Komura · 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
- Primero, la conclusión (en una frase)
- 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
- 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
- 3.1. La comprobación en dos pasos
- 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
- 4.1. Publicar con
- Pseudocódigo (extracto)
- Guía rápida de uso según el caso
- Resumen
- 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.
sequenceDiagram
accTitle: Lectura de un archivo mientras todavía se está escribiendo
accDescr: Diagrama 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
participant Emisor as "Lado emisor"
participant Compartida as "Carpeta compartida"
participant Receptor as "Lado receptor"
Emisor->>Compartida: crea orders.csv con el nombre final
Emisor->>Compartida: está escribiendo las filas 1 a 5000
Receptor->>Compartida: detecta orders.csv
Receptor->>Compartida: empieza a leerlo tal cual
Note over Receptor: todavía está a medias
Emisor->>Compartida: escribe el resto
Note over Receptor: filas insuficientes / fallo de análisis / procesado solo en parte
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.
sequenceDiagram
accTitle: Dos workers tomando el mismo archivo a la vez
accDescr: Diagrama 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 duplicado
participant W1 as "Worker 1"
participant W2 as "Worker 2"
participant Dir as incoming
W1->>Dir: encuentra a.csv
W2->>Dir: encuentra a.csv
W1->>Dir: empieza a leer
W2->>Dir: empieza a leer
Note over W1,W2: procesa la misma entrada por duplicado
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.
sequenceDiagram
accTitle: Un stale lock detiene a todos los workers
accDescr: Diagrama 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
participant A as "Worker A"
participant Lock as "archivo lock"
participant B as "Worker B"
A->>Lock: crea el lock
Note over A: aquí termina de forma anómala
B->>Lock: comprueba si el lock existe
B->>Lock: pospone el inicio del procesamiento
B->>Lock: sigue esperando
Note over B,Lock: no se puede determinar si es stale y todos quedan detenidos
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.
sequenceDiagram
accTitle: La comprobación en dos pasos Exists -> Create no es exclusión
accDescr: Diagrama 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
participant A as "Proceso A"
participant B as "Proceso B"
participant FS as "Sistema de archivos"
A->>FS: comprueba si no existe el lock
B->>FS: comprueba si no existe el lock
FS-->>A: no existe
FS-->>B: no existe
A->>FS: crea el lock
B->>FS: crea el lock
Note over A,B: ambos terminan avanzando
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.
flowchart LR
accTitle: El nombre final visible no significa que el contenido esté completo
accDescr: Diagrama 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 incompletos
A[El nombre final se vuelve visible] --> B[El lado receptor lo detecta]
B --> C[El lado emisor todavía está escribiendo]
C --> D[Se 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.
sequenceDiagram
accTitle: Dar por completado un archivo cuyo tamaño solo dejó de cambiar temporalmente
accDescr: Diagrama 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
participant Emisor as "Lado emisor"
participant Compartida as "Carpeta compartida"
participant Receptor as "Lado receptor"
Emisor->>Compartida: empieza a copiar data.zip
Emisor->>Compartida: se pausa a medio camino
Receptor->>Compartida: el tamaño no cambia en 10 segundos
Note over Receptor: lo interpreta erróneamente como completado
Receptor->>Compartida: empieza a leer
Emisor->>Compartida: reanuda 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.
sequenceDiagram
accTitle: Dos batches actualizando el mismo archivo compartido
accDescr: Diagrama 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
participant A as "Batch A"
participant B as "Batch B"
participant F as "status.csv"
A->>F: lee v1
B->>F: lee v1
A->>F: escribe v2-A
B->>F: escribe v2-B
Note over F: la actualización de A se pierde
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
flockde 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.
flowchart LR
accTitle: Publicación mediante temp -> close -> rename / replace
accDescr: Diagrama 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 final
A[Crear un nombre temp único] --> B[Escribir todo el contenido en temp]
B --> C[Hacer flush / close]
C --> D[Rename / replace al nombre final en el mismo directorio]
D --> E[El 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\shareAa\\server\shareB, se trata como un volumen distinto, y elMoveFileExde Windows, cuando se especificaMOVEFILE_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í comoincomingyprocessing, 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.
flowchart TD
accTitle: Explicitar la integridad con done / manifest
accDescr: Diagrama 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 hash
A[Generar data.tmp] --> B[Publicar como data.csv]
B --> C[Crear data.done / manifest.json]
C --> D[El lado receptor detecta done / manifest]
D --> E[Verificar 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.
sequenceDiagram
accTitle: El lado receptor toma el claim mediante rename atómico
accDescr: Diagrama 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 propietario
participant W1 as "Worker 1"
participant W2 as "Worker 2"
participant IN as incoming
participant PR as processing
W1->>IN: encuentra a.csv
W2->>IN: encuentra a.csv
W1->>PR: renombra a.csv
W2->>PR: renombra a.csv
Note over W1,W2: solo el que tuvo éxito primero toma la propiedad
En la operación, separar también los directorios facilita el seguimiento.
flowchart LR
accTitle: Separación de directorios en temp, incoming, processing, archive y error
accDescr: Diagrama 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 falla
T[temp] -->|publish| I[incoming]
I -->|claim| P[processing]
P -->|éxito| A[archive]
P -->|fallo| E[error]
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.
flowchart TD
accTitle: Campos de un lock file convertido en lease
accDescr: Diagrama de flujo que muestra los seis campos que componen lock.json como lease, ownerId, host, pid, acquiredAt, expiresAt y heartbeatAt
L[lock.json] --> A[ownerId]
L --> B[host]
L --> C[pid]
L --> D[acquiredAt]
L --> E[expiresAt]
L --> F[heartbeatAt]
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.
flowchart LR
accTitle: Diseño de procesamiento basado en idempotency
accDescr: Diagrama 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 procesados
A[Entrada + idempotency key] --> B{¿Ya se procesó?}
B -- sí --> C[Se trata como éxito sin reejecutar]
B -- no --> D[Se ejecuta el procesamiento]
D --> E[Se 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.
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.
ExistsyWriteAllTextson operaciones separadasfinalPathse vuelve visible desde mitad de la escritura- En caso de terminación anómala, el
lockqueda 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 -> renamese 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
- Conjunto de código de ejemplo de este artículo (librería, demo, pruebas unitarias) - komurasoft-blog-samples (GitHub)
- LockFileEx function (Win32)
- Locking and Unlocking Byte Ranges in Files (Win32)
- Moving and Replacing Files (Win32)
- MoveFileEx function (Win32)
- File Times (Win32)
- FileStream.Lock Method (.NET)
- File.Replace Method (.NET)
- rename — POSIX
- open — POSIX (
O_CREAT | O_EXCL) - flock(2) — Linux manual page
- open(2) — Linux manual page
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Guía práctica de FileSystemWatcher - Cómo evitar pérdidas de eventos y duplicados
Analizamos el uso de FileSystemWatcher: pérdida de eventos, notificaciones duplicadas, trampas de finalización, reescaneo, claim atómico ...
Diseño de códigos en sistemas empresariales ── Cómo definir códigos de producto y cliente, y el dígito de control
Guía práctica para diseñar códigos de producto y cliente en sistemas empresariales: código significativo frente a secuencial, fórmulas de...
Por qué priorizar la espera por eventos sobre Sleep(1) en Windows
En Windows, la precisión de una espera corta con timeout depende de la granularidad del reloj del sistema y de la planificación. Este art...
Tabla de decisión: terminar o continuar ante una excepción inesperada
Analiza si una aplicación debe terminar o continuar tras una excepción inesperada, según el daño al estado, los efectos externos, los hil...
Por qué usar .NET Generic Host y BackgroundService en una aplicación de escritorio
Resumimos cómo usar Generic Host y BackgroundService en herramientas y apps residentes de Windows para organizar el inicio, el procesamie...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
En el desarrollo de aplicaciones Windows que incluye integración por carpetas compartidas y batches nocturnos, el diseño del control de exclusión repercute directamente en la calidad de la implementación.
Consultoría técnica y revisión de diseño
Si quiere ordenar de antemano la separación de responsabilidades entre lock, claim atómico e idempotency, esto puede tratarse como consultoría técnica y revisión de diseño.
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.