diff --git a/src/dm/dm_mailbox_media.c b/src/dm/dm_mailbox_media.c index 367412305..df3e49811 100644 --- a/src/dm/dm_mailbox_media.c +++ b/src/dm/dm_mailbox_media.c @@ -2,6 +2,7 @@ #include #include #include +#include #include "../../lib/mem.h" #include "../../lib/debug_config.h" @@ -111,18 +112,24 @@ static void dm_job_receipt(struct dm_mb_media* media, uint64_t group, uint64_t s } /* Освобождение назначения и сброс текущего holder — один durable переход. */ -static int dm_job_release(struct dm_mb_media* media, const uint8_t id[16], uint64_t holder, int expired) { +static int dm_job_release(struct dm_mb_media* media, const uint8_t id[16], uint64_t holder, + const uint8_t assignment[16], int expired) { struct UTUN_INSTANCE* inst = media->inst; if (dm_media_exec(inst, "BEGIN IMMEDIATE")) return -1; - const char* sql[] = { "UPDATE dm_media_holders SET released=1 WHERE id=? AND node_id=? AND released=0", expired ? - "UPDATE dm_media_jobs SET holder=0,stored=0,expired=1,retry_at=0 WHERE id=? AND holder=? AND receipt IS NULL" : - "UPDATE dm_media_jobs SET holder=0,stored=0,retry_at=0 WHERE id=? AND holder=? AND receipt IS NULL" }; + const char* sql[] = { expired ? + "UPDATE dm_media_holders SET released=2 WHERE id=? AND node_id=? AND assignment_id=? AND released=0" : + "UPDATE dm_media_holders SET released=3 WHERE id=? AND node_id=? AND assignment_id=? AND released=0", expired ? + "UPDATE dm_media_jobs SET holder=0,stored=0,expired=1,retry_at=0 WHERE id=?1 AND holder=?2 AND receipt IS NULL" + " AND EXISTS(SELECT 1 FROM dm_media_holders h WHERE h.id=?1 AND h.node_id=?2 AND h.assignment_id=?3)" : + "UPDATE dm_media_jobs SET holder=0,stored=0,expires=0,delete_on_open=-1,retry_at=0 WHERE id=?1 AND holder=?2 AND receipt IS NULL" + " AND EXISTS(SELECT 1 FROM dm_media_holders h WHERE h.id=?1 AND h.node_id=?2 AND h.assignment_id=?3)" }; int changed[2]; for (int i = 0; i < 2; i++) { sqlite3_stmt* st = dm_media_prepare(inst, sql[i]); if (!st) goto rollback; int rc = sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC); if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)holder); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, assignment, 16, SQLITE_STATIC); if (rc == SQLITE_OK) rc = sqlite3_step(st); int final = sqlite3_finalize(st); if (rc != SQLITE_DONE || final != SQLITE_OK) { @@ -150,23 +157,36 @@ void dm_mailbox_media_recv(struct dm_mb_media* media, uint64_t group, uint64_t s if (!media) { DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: control after stop"); return; } struct UTUN_INSTANCE* inst = media->inst; if (cmd == DM_MEDIA_RECEIPT && len == DM_MEDIA_RECEIPT_SIZE) { dm_job_receipt(media, group, source, p); return; } - sqlite3_stmt* st = dm_media_prepare(inst, "SELECT j.author,j.recipient,j.receipt,j.holder FROM dm_media_jobs j" - " JOIN dm_media_holders h ON h.id=j.id WHERE j.id=? AND h.node_id=?"); + if (!p || !(((cmd == DM_MEDIA_STORED || cmd == DM_MEDIA_RESERVED) && len == 41) || + ((cmd == DM_MEDIA_DELETE_ACK || cmd == DM_MEDIA_EXPIRED) && len == 32) || (cmd == DM_MEDIA_REJECT && len == 33))) { + DEBUG_WARN(DEBUG_CATEGORY_DM, "DM assignment: invalid response cmd=%u bytes=%zu", cmd, len); return; + } + const uint8_t* assignment = p; p += 16; len -= 16; + sqlite3_stmt* st = dm_media_prepare(inst, "SELECT j.receipt,j.holder,h.released FROM dm_media_jobs j" + " JOIN dm_media_holders h ON h.id=j.id WHERE j.id=? AND h.node_id=? AND h.assignment_id=?"); if (!st) return; sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)source); + sqlite3_bind_blob(st, 3, assignment, 16, SQLITE_STATIC); int rc = sqlite3_step(st); - if (rc == SQLITE_DONE) { sqlite3_finalize(st); return; } + if (rc == SQLITE_DONE) { + DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM assignment: unknown/stale response M=%llu cmd=%u", (unsigned long long)source, cmd); + sqlite3_finalize(st); return; + } if (rc != SQLITE_ROW) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: location lookup failed rc=%d", rc); sqlite3_finalize(st); return; } - uint64_t holder = (uint64_t)sqlite3_column_int64(st, 3); + uint64_t holder = (uint64_t)sqlite3_column_int64(st, 1); + int released = sqlite3_column_int(st, 2); uint8_t receipt[DM_MEDIA_RECEIPT_SIZE]; - int delivered = sqlite3_column_bytes(st, 2) == sizeof(receipt); - if (delivered) memcpy(receipt, sqlite3_column_blob(st, 2), sizeof(receipt)); + int delivered = sqlite3_column_bytes(st, 0) == sizeof(receipt); + if (delivered) memcpy(receipt, sqlite3_column_blob(st, 0), sizeof(receipt)); sqlite3_finalize(st); if ((cmd == DM_MEDIA_STORED || cmd == DM_MEDIA_RESERVED) && len == 25 && p[24] <= 1) { uint64_t expiry; memcpy(&expiry, p + 16, 8); - if (delivered) { dm_media_send(inst, group, source, DM_MEDIA_DELETE, receipt, sizeof(receipt)); return; } - if (holder != source || expiry <= ntp_time_get_seconds(inst)) { + if (delivered) { + uint8_t request[16 + DM_MEDIA_RECEIPT_SIZE]; memcpy(request, assignment, 16); memcpy(request + 16, receipt, sizeof(receipt)); + dm_media_send(inst, group, source, DM_MEDIA_DELETE, request, sizeof(request)); return; + } + if (released || holder != source || expiry <= ntp_time_get_seconds(inst)) { DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: stale/unauthorized STORED source=%llu expiry=%llu", (unsigned long long)source, (unsigned long long)expiry); return; } @@ -180,11 +200,12 @@ void dm_mailbox_media_recv(struct dm_mb_media* media, uint64_t group, uint64_t s DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: custody committed M=%llu expires=%llu", (unsigned long long)source, (unsigned long long)expiry); } else if ((cmd == DM_MEDIA_DELETE_ACK || cmd == DM_MEDIA_EXPIRED) && len == 16) { - if (cmd == DM_MEDIA_EXPIRED && !delivered) { dm_job_release(media, p, source, 1); return; } - st = dm_media_prepare(inst, "UPDATE dm_media_holders SET released=1 WHERE id=? AND node_id=? AND released=0"); + if (cmd == DM_MEDIA_EXPIRED && !delivered) { dm_job_release(media, p, source, assignment, 1); return; } + st = dm_media_prepare(inst, "UPDATE dm_media_holders SET released=1 WHERE id=? AND node_id=? AND assignment_id=? AND released=0"); if (!st) return; rc = sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)source); + if (rc == SQLITE_OK) rc = sqlite3_bind_blob(st, 3, assignment, 16, SQLITE_STATIC); if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "dm_mailbox_media: ACK bind failed M=%llu cmd=%u rc=%d", (unsigned long long)source, cmd, rc); @@ -194,7 +215,7 @@ void dm_mailbox_media_recv(struct dm_mb_media* media, uint64_t group, uint64_t s DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: assignment released M=%llu cmd=%u", (unsigned long long)source, cmd); } else if (cmd == DM_MEDIA_REJECT && len == 17 && !delivered) { DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: storage refused M=%llu reason=%u", (unsigned long long)source, p[16]); - dm_job_release(media, p, source, 0); + if (!released) dm_job_release(media, p, source, assignment, 0); } else DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_mailbox_media: unexpected control cmd=%u bytes=%zu", cmd, len); } @@ -236,7 +257,7 @@ static uint64_t dm_job_storage(struct dm_mb_media* media, const uint8_t id[16]) /* Удаление неосвобождённых назначений повторяется до DELETE_ACK; истёкшие завершаются по TTL. */ static void dm_job_cleanup(struct dm_mb_media* media, const uint8_t id[16], int terminal) { struct UTUN_INSTANCE* inst = media->inst; - sqlite3_stmt* st = dm_media_prepare(inst, "SELECT node_id FROM dm_media_holders WHERE id=? AND released=0 LIMIT 16"); + sqlite3_stmt* st = dm_media_prepare(inst, "SELECT node_id,assignment_id FROM dm_media_holders WHERE id=? AND released=0 LIMIT 16"); if (!st) return; int rc = sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC); if (rc != SQLITE_OK) { @@ -244,9 +265,11 @@ static void dm_job_cleanup(struct dm_mb_media* media, const uint8_t id[16], int sqlite3_finalize(st); return; } uint64_t peers[16]; + uint8_t assignments[16][16]; unsigned count = 0; while ((rc = sqlite3_step(st)) == SQLITE_ROW && count < 16) { - peers[count++] = (uint64_t)sqlite3_column_int64(st, 0); + if (sqlite3_column_bytes(st, 1) != 16) { rc = SQLITE_CORRUPT; break; } + peers[count] = (uint64_t)sqlite3_column_int64(st, 0); memcpy(assignments[count++], sqlite3_column_blob(st, 1), 16); } int final = sqlite3_finalize(st); if ((rc != SQLITE_ROW && rc != SQLITE_DONE) || final != SQLITE_OK) { @@ -263,8 +286,10 @@ static void dm_job_cleanup(struct dm_mb_media* media, const uint8_t id[16], int } sqlite3_finalize(st); if (rc != SQLITE_DONE) { DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM custody: corrupt/overflowing receipt set rc=%d", rc); return; } - for (unsigned i = 0; i < count; i++) for (unsigned j = 0; j < fact_count; j++) - dm_media_send(inst, 0, peers[i], DM_MEDIA_DELETE, facts[j], DM_MEDIA_RECEIPT_SIZE); + for (unsigned i = 0; i < count; i++) for (unsigned j = 0; j < fact_count; j++) { + uint8_t request[16 + DM_MEDIA_RECEIPT_SIZE]; memcpy(request, assignments[i], 16); memcpy(request + 16, facts[j], sizeof(facts[j])); + dm_media_send(inst, 0, peers[i], DM_MEDIA_DELETE, request, sizeof(request)); + } if (!count && terminal) { st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET body=NULL WHERE id=? AND receipt IS NOT NULL AND body IS NOT NULL" " AND NOT EXISTS(SELECT 1 FROM dm_media_holders h WHERE h.id=dm_media_jobs.id AND h.released=0)"); @@ -279,6 +304,36 @@ static void dm_job_cleanup(struct dm_mb_media* media, const uint8_t id[16], int } } +/* Не получившие файл устройства узнают об удалённой копии; отказ в резерве не означает удаления. */ +static void dm_job_source_notices(struct UTUN_INSTANCE* inst, const uint8_t id[16]) { + sqlite3_stmt* st = dm_media_prepare(inst, "SELECT h.node_id,j.body_hash,t.node FROM dm_media_holders h" + " JOIN dm_media_jobs j ON j.id=h.id JOIN dm_file_targets t ON t.id=h.id" + " WHERE h.id=? AND h.released IN (1,2) AND h.node_id<>j.author AND t.received IS NULL"); + if (!st) return; + struct source_notice { uint64_t peer; uint8_t body[56]; } *notices = NULL; + size_t count = 0, capacity = 0; + int rc = sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC); + if (rc == SQLITE_OK) while ((rc = sqlite3_step(st)) == SQLITE_ROW) { + if (sqlite3_column_bytes(st, 1) != 32) { rc = SQLITE_CORRUPT; break; } + if (count == capacity) { + size_t next = capacity ? capacity * 2 : 8; + struct source_notice* grown = u_realloc(notices, next * sizeof(*notices)); + if (!grown) { rc = SQLITE_NOMEM; break; } + notices = grown; capacity = next; + } + uint64_t holder = (uint64_t)sqlite3_column_int64(st, 0); + notices[count].peer = (uint64_t)sqlite3_column_int64(st, 2); + memcpy(notices[count].body, id, 16); memcpy(notices[count].body + 16, sqlite3_column_blob(st, 1), 32); + memcpy(notices[count++].body + 48, &holder, 8); + } + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM sources: terminal assignment snapshot failed rc=%d final=%d", rc, final); + } else for (size_t i = 0; i < count; i++) + dm_media_send(inst, 0, notices[i].peer, DM_MEDIA_SOURCE_GONE, notices[i].body, sizeof(notices[i].body)); + u_free(notices); +} + /* Snapshot statement закрывается до control send, включая локальное совмещение S/M. */ static void dm_mailbox_media_tick(void* arg) { struct dm_mb_media* media = arg; @@ -286,8 +341,9 @@ static void dm_mailbox_media_tick(void* arg) { media->timer = NULL; uint64_t now = ntp_time_get_seconds(inst); for (int i = 0; i < 8; i++) { - sqlite3_stmt* st = dm_media_prepare(inst, "SELECT id,author,recipient,body,holder,stored,expires,receipt,expired,delete_on_open" - " FROM dm_media_jobs WHERE retry_at<=? ORDER BY retry_at,seq LIMIT 1"); + sqlite3_stmt* st = dm_media_prepare(inst, "SELECT j.id,j.author,j.recipient,j.body,j.holder,j.stored,j.expires,j.receipt," + "j.expired,j.delete_on_open,h.assignment_id,h.released FROM dm_media_jobs j LEFT JOIN dm_media_holders h" + " ON h.id=j.id AND h.node_id=j.holder WHERE j.retry_at<=? ORDER BY j.retry_at,j.seq LIMIT 1"); if (!st) break; sqlite3_bind_int64(st, 1, (sqlite3_int64)now); int rc = sqlite3_step(st); @@ -301,6 +357,12 @@ static void dm_mailbox_media_tick(void* arg) { size_t len = (size_t)sqlite3_column_bytes(st, 3); if (len <= sizeof(body) && len) memcpy(body, sqlite3_column_blob(st, 3), len); uint64_t holder = (uint64_t)sqlite3_column_int64(st, 4); + uint8_t assignment[16]; + if (holder && sqlite3_column_bytes(st, 10) != 16) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM assignment: missing/corrupt ID M=%llu", (unsigned long long)holder); sqlite3_finalize(st); break; + } + if (holder) memcpy(assignment, sqlite3_column_blob(st, 10), 16); + int released = sqlite3_column_int(st, 11); int stored = sqlite3_column_int(st, 5), expired = sqlite3_column_int(st, 8); int policy = sqlite3_column_int(st, 9); uint64_t expires = (uint64_t)sqlite3_column_int64(st, 6); @@ -311,6 +373,7 @@ static void dm_mailbox_media_tick(void* arg) { if (!st) break; sqlite3_bind_int64(st, 1, (sqlite3_int64)(now + 5)); sqlite3_bind_blob(st, 2, id, 16, SQLITE_STATIC); if (dm_media_step(inst, st)) break; + dm_job_source_notices(inst, id); if (!delivered && policy >= 0 && dm_media_retention_done(inst, id, policy) == 1) { st = dm_media_prepare(inst, policy ? "UPDATE dm_media_jobs SET receipt=(SELECT taken FROM dm_file_targets WHERE id=? AND side=1 AND taken IS NOT NULL LIMIT 1)" @@ -329,7 +392,7 @@ static void dm_mailbox_media_tick(void* arg) { continue; } if (holder && expires && expires <= now) { - if (!dm_job_release(media, id, holder, 1)) + if (!dm_job_release(media, id, holder, assignment, 1)) DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: custody deadline reconciled M=%llu expires=%llu now=%llu", (unsigned long long)holder, (unsigned long long)expires, (unsigned long long)now); continue; /* Новый снимок после commit; при ошибке задача повторится через retry_at. */ @@ -348,22 +411,25 @@ static void dm_mailbox_media_tick(void* arg) { uint64_t gid = 0; if (!dm_route_group(inst, targets[t], 0, &gid)) continue; uint8_t offer[24 + DM_MEDIA_BODY_MAX]; - uint64_t location = stored && expires > now ? holder : author; - uint64_t expiry = stored ? expires : 0; + uint64_t location = stored && !released && expires > now ? holder : author; + uint64_t expiry = stored && !released ? expires : 0; memcpy(offer, targets + t, 8); memcpy(offer + 8, &location, 8); memcpy(offer + 16, &expiry, 8); memcpy(offer + 24, body, len); dm_media_send(inst, gid, targets[t], DM_MEDIA_OFFER, offer, len + 24); - if (expired || (stored && expires <= now)) dm_media_send(inst, gid, targets[t], DM_MEDIA_EXPIRED, id, 16); } if (!expired) { if (!holder) { if (!dm_peer_role(inst, inst->node_id, "supernode")) continue; holder = dm_job_storage(media, id); if (!holder) continue; + if (RAND_bytes(assignment, sizeof(assignment)) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_DM, "DM assignment: random ID generation failed"); continue; + } if (dm_media_exec(inst, "BEGIN IMMEDIATE")) continue; - st = dm_media_prepare(inst, "INSERT INTO dm_media_holders(id,node_id) VALUES(?,?)"); + st = dm_media_prepare(inst, "INSERT INTO dm_media_holders(id,node_id,assignment_id) VALUES(?,?,?)"); if (!st) { dm_media_exec(inst, "ROLLBACK"); continue; } sqlite3_bind_blob(st, 1, id, 16, SQLITE_STATIC); sqlite3_bind_int64(st, 2, (sqlite3_int64)holder); + sqlite3_bind_blob(st, 3, assignment, 16, SQLITE_STATIC); if (dm_media_step(inst, st)) { dm_media_exec(inst, "ROLLBACK"); continue; } st = dm_media_prepare(inst, "UPDATE dm_media_jobs SET holder=?,stored=0 WHERE id=? AND receipt IS NULL"); if (!st) { dm_media_exec(inst, "ROLLBACK"); continue; } @@ -371,10 +437,8 @@ static void dm_mailbox_media_tick(void* arg) { if (dm_media_step(inst, st) || dm_media_exec(inst, "COMMIT")) { dm_media_exec(inst, "ROLLBACK"); continue; } DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_mailbox_media: assigned storage M=%llu", (unsigned long long)holder); } - if (!stored) { - uint8_t put[8 + DM_MEDIA_BODY_MAX]; memcpy(put, &recipient, 8); memcpy(put + 8, body, len); - dm_media_send(inst, 0, holder, DM_MEDIA_STORE, put, len + 8); - } + uint8_t put[24 + DM_MEDIA_BODY_MAX]; memcpy(put, assignment, 16); memcpy(put + 16, &recipient, 8); memcpy(put + 24, body, len); + dm_media_send(inst, 0, holder, DM_MEDIA_STORE, put, len + 24); } } media->timer = uasync_set_timeout(inst->ua, 10000, media, dm_mailbox_media_tick, "dm_mailbox_media"); diff --git a/src/dm/dm_media.c b/src/dm/dm_media.c index f18f46d85..34ab30788 100644 --- a/src/dm/dm_media.c +++ b/src/dm/dm_media.c @@ -1231,8 +1231,8 @@ static void dm_custody_finalize(struct dm_media_state* media, const uint8_t id[1 } if (dm_media_step(inst, st) || dm_media_exec(inst, "COMMIT")) goto rollback; dm_custody_notify(inst, id); - DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_media: custody copy removed reason=%s super=%llu", - delivered ? "delivered" : "expired", (unsigned long long)super); + DEBUG_INFO(DEBUG_CATEGORY_DM, "dm_media: custody copy removed reason=%s; all coordinators notified", + delivered ? "delivered" : "expired"); return; rollback: DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_media: custody retirement rolled back; retry retained, no ACK"); @@ -1320,6 +1320,8 @@ static void dm_media_offer(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t DEBUG_WARN(DEBUG_CATEGORY_DM, "dm_media: unauthorized OFFER source=%llu holder=%llu", (unsigned long long)source, (unsigned long long)holder); return; } + /* На узле автора оригинал и custody — разные копии; срок custody не ограничивает поиск оригинала. */ + if (holder == author) expiry = 0; uint8_t receipt[DM_RECEIPT_SIZE]; if (dm_accept_message(inst, p, len, receipt)) return; dm_mailbox_receipt(inst, receipt); @@ -1362,16 +1364,20 @@ static void dm_source_gone(struct UTUN_INSTANCE* inst, uint64_t source, const ui DEBUG_WARN(DEBUG_CATEGORY_DM, "DM source: unauthorized removal source=%llu holder=%llu", (unsigned long long)source, (unsigned long long)holder); return; } - sqlite3_stmt* st = dm_media_prepare(inst, "SELECT body FROM dm_files WHERE id=?"); + sqlite3_stmt* st = dm_media_prepare(inst, "SELECT body,author FROM dm_files WHERE id=?"); if (!st) return; int rc = sqlite3_bind_blob(st, 1, p, 16, SQLITE_STATIC); if (rc == SQLITE_OK) rc = sqlite3_step(st); uint8_t hash[32]; if (rc == SQLITE_ROW) SHA256(sqlite3_column_blob(st, 0), (size_t)sqlite3_column_bytes(st, 0), hash); + int original = rc == SQLITE_ROW && (uint64_t)sqlite3_column_int64(st, 1) == holder; int exact = rc == SQLITE_ROW && !memcmp(hash, p + 16, 32), final = sqlite3_finalize(st); if (!exact || final != SQLITE_OK) { DEBUG_WARN(DEBUG_CATEGORY_DM, "DM source: removal for unknown/conflicting file rc=%d final=%d", rc, final); return; } + if (original) { + DEBUG_DEBUG(DEBUG_CATEGORY_DM, "DM source: custody ended on author=%llu; original remains eligible", (unsigned long long)holder); return; + } if (dm_media_exec(inst, "BEGIN IMMEDIATE")) return; st = dm_media_prepare(inst, "INSERT INTO dm_file_sources(id,node,expires) VALUES(?,?,-1)" " ON CONFLICT(id,node) DO UPDATE SET expires=-1"); @@ -1386,8 +1392,7 @@ static void dm_source_gone(struct UTUN_INSTANCE* inst, uint64_t source, const ui if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, (sqlite3_int64)holder); if (rc != SQLITE_OK) { sqlite3_finalize(st); goto rollback; } if (dm_media_step(inst, st) || dm_media_exec(inst, "COMMIT")) goto rollback; - struct dm_file_probe* probe = dm_probe_find(inst->dm_media, p); - if (probe) for (unsigned i = 0; i < probe->count; i++) if (probe->sources[i].node == holder) probe->sources[i].used = 1; + /* HAVE от устройства стороны может относиться к его личной реплике; статус custody её не отменяет. */ DEBUG_INFO(DEBUG_CATEGORY_DM, "DM source: retired holder=%llu; other sources and local file retained", (unsigned long long)holder); return; rollback: