[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-it-postgres-advisory-lock-connection-pool-leak":3},{"id":4,"title":5,"author":6,"body":7,"cover":1060,"description":1061,"draft":1062,"extension":1063,"locale":1064,"meta":1065,"navigation":108,"path":1066,"publishedAt":1067,"seo":1068,"seoTitle":1069,"slug":1070,"stem":1071,"tags":1072,"__hash__":1076},"blog\u002Fit\u002Fblog\u002Fpostgres-advisory-lock-connection-pool-leak.md","Advisory lock e pool di connessioni: il leak silenzioso e come evitarlo","Manuel Tardivo",{"type":8,"value":9,"toc":1041},"minimark",[10,15,38,64,75,79,86,157,160,175,183,189,194,219,248,255,263,283,291,298,312,318,343,359,391,407,427,438,444,455,517,524,530,559,563,582,587,600,607,613,627,645,662,666,687,694,861,866,940,947,951,964,968,992,996,1037],[11,12,14],"h2",{"id":13},"per-chi-è-questo-pezzo","Per chi è questo pezzo",[16,17,18,19,23,24,28,29,32,33,37],"p",{},"Se hai ",[20,21,22],"strong",{},"worker che a volte eseguono lo stesso job due volte",", o stai scegliendo come serializzare una coda su Postgres, probabilmente sai già cosa sono un advisory lock e un ",[25,26,27],"code",{},"SELECT … FOR UPDATE",". Quello che ",[20,30,31],{},"non"," trovi facilmente in documentazione è ",[34,35,36],"em",{},"quando il meccanismo che hai scelto ti tradisce in produzione"," e come accorgertene prima che i sintomi siano “dati doppi” o richieste appese.",[16,39,40,41,44,45,48,49,55,56,59,60,63],{},"L’angolo qui è uno solo: ",[20,42,43],{},"lock advisory session-scoped + pool che riusa la stessa sessione Postgres tra transazioni"," (il caso tipico di ",[20,46,47],{},"PgBouncer in transaction pooling","), e perché ",[20,50,51,54],{},[25,52,53],{},"SKIP LOCKED"," è l’anti-pattern"," se il tuo obiettivo è ",[34,57,58],{},"attendere"," la stessa chiave, non ",[34,61,62],{},"saltarla",".",[16,65,66,67,70,71,74],{},"Riferimento di versione: esempi allineati a ",[20,68,69],{},"PostgreSQL 16","; su altre major controlla i campi di ",[25,72,73],{},"pg_locks"," e le note del tuo pooler.",[11,76,78],{"id":77},"setup-minimo-ledger","Setup minimo (ledger)",[16,80,81,82,85],{},"Serve una riga stabile per chiave logica se più avanti confrontiamo con ",[25,83,84],{},"FOR UPDATE",":",[87,88,93],"pre",{"className":89,"code":90,"language":91,"meta":92,"style":92},"language-sql shiki shiki-themes github-light github-dark","DROP TABLE IF EXISTS job_ledger;\n\nCREATE TABLE job_ledger (\n  job_key     text PRIMARY KEY,\n  claim_count bigint NOT NULL DEFAULT 0,\n  updated_at  timestamptz NOT NULL DEFAULT now()\n);\n\nINSERT INTO job_ledger (job_key) VALUES ('report-daily')\nON CONFLICT DO NOTHING;\n","sql","",[25,94,95,103,110,116,122,128,134,140,145,151],{"__ignoreMap":92},[96,97,100],"span",{"class":98,"line":99},"line",1,[96,101,102],{},"DROP TABLE IF EXISTS job_ledger;\n",[96,104,106],{"class":98,"line":105},2,[96,107,109],{"emptyLinePlaceholder":108},true,"\n",[96,111,113],{"class":98,"line":112},3,[96,114,115],{},"CREATE TABLE job_ledger (\n",[96,117,119],{"class":98,"line":118},4,[96,120,121],{},"  job_key     text PRIMARY KEY,\n",[96,123,125],{"class":98,"line":124},5,[96,126,127],{},"  claim_count bigint NOT NULL DEFAULT 0,\n",[96,129,131],{"class":98,"line":130},6,[96,132,133],{},"  updated_at  timestamptz NOT NULL DEFAULT now()\n",[96,135,137],{"class":98,"line":136},7,[96,138,139],{},");\n",[96,141,143],{"class":98,"line":142},8,[96,144,109],{"emptyLinePlaceholder":108},[96,146,148],{"class":98,"line":147},9,[96,149,150],{},"INSERT INTO job_ledger (job_key) VALUES ('report-daily')\n",[96,152,154],{"class":98,"line":153},10,[96,155,156],{},"ON CONFLICT DO NOTHING;\n",[16,158,159],{},"Costante applicativa per advisory (evita collisioni con altri sottosistemi):",[87,161,163],{"className":89,"code":162,"language":91,"meta":92,"style":92},"-- classid fittizio condiviso da tutti i lock “job queue” dell’app\nSELECT 7742 AS app_advisory_class;\n",[25,164,165,170],{"__ignoreMap":92},[96,166,167],{"class":98,"line":99},[96,168,169],{},"-- classid fittizio condiviso da tutti i lock “job queue” dell’app\n",[96,171,172],{"class":98,"line":105},[96,173,174],{},"SELECT 7742 AS app_advisory_class;\n",[11,176,178,179,182],{"id":177},"dimostrazione-breve-pg_advisory_xact_lock-fa-quello-che-ti-aspetti","Dimostrazione breve: ",[25,180,181],{},"pg_advisory_xact_lock"," fa quello che ti aspetti",[16,184,185,186,63],{},"Due finestre ",[25,187,188],{},"psql",[16,190,191],{},[20,192,193],{},"Sessione A",[87,195,197],{"className":89,"code":196,"language":91,"meta":92,"style":92},"BEGIN;\nSELECT pg_advisory_xact_lock(7742, hashtext('report-daily'));\n-- … lavoro …\nCOMMIT;\n",[25,198,199,204,209,214],{"__ignoreMap":92},[96,200,201],{"class":98,"line":99},[96,202,203],{},"BEGIN;\n",[96,205,206],{"class":98,"line":105},[96,207,208],{},"SELECT pg_advisory_xact_lock(7742, hashtext('report-daily'));\n",[96,210,211],{"class":98,"line":112},[96,212,213],{},"-- … lavoro …\n",[96,215,216],{"class":98,"line":118},[96,217,218],{},"COMMIT;\n",[16,220,221,224,225,228,229,232,233,236,237,240,241,243,244,247],{},[20,222,223],{},"Sessione B"," (stesso ",[25,226,227],{},"BEGIN"," + stessa ",[25,230,231],{},"SELECT pg_advisory_xact_lock(…)"," mentre A non ha ancora fatto ",[25,234,235],{},"COMMIT","): ",[20,238,239],{},"resta in attesa",". Dopo ",[25,242,235],{}," di A, B prosegue. Il lock è legato alla ",[20,245,246],{},"transazione",": a fine transazione non resta nulla sul backend.",[16,249,250,251,254],{},"Fin qui niente di nuovo rispetto alla doc — serve solo come ",[20,252,253],{},"controllo positivo"," prima del caso che brucia.",[11,256,258,259,262],{"id":257},"il-caso-che-brucia-pg_advisory_lock-e-il-pool-in-transaction-mode","Il caso che brucia: ",[25,260,261],{},"pg_advisory_lock"," e il pool in transaction mode",[16,264,265,267,268,271,272,275,276,278,279,282],{},[25,266,261],{}," (senza ",[25,269,270],{},"xact_",") è ",[20,273,274],{},"session-scoped",": sopravvive al ",[25,277,235],{}," finché non chiami ",[25,280,281],{},"pg_advisory_unlock"," o non chiudi la connessione.",[284,285,287,288,290],"h3",{"id":286},"riproduzione-senza-pgbouncer-stesso-backend-una-sola-psql","Riproduzione senza PgBouncer (stesso backend, una sola ",[25,289,188],{},")",[16,292,293,294,297],{},"In ",[20,295,296],{},"una"," sessione:",[87,299,301],{"className":89,"code":300,"language":91,"meta":92,"style":92},"SELECT pg_advisory_lock(7742, hashtext('report-daily'));\nCOMMIT;\n",[25,302,303,308],{"__ignoreMap":92},[96,304,305],{"class":98,"line":99},[96,306,307],{},"SELECT pg_advisory_lock(7742, hashtext('report-daily'));\n",[96,309,310],{"class":98,"line":105},[96,311,218],{},[16,313,314,315,317],{},"Dopo il ",[25,316,235],{},", chiedi a Postgres se il lock c’è ancora:",[87,319,321],{"className":89,"code":320,"language":91,"meta":92,"style":92},"SELECT l.pid, l.mode, l.granted, l.classid, l.objid\nFROM pg_locks l\nWHERE l.locktype = 'advisory'\n  AND l.pid = pg_backend_pid();\n",[25,322,323,328,333,338],{"__ignoreMap":92},[96,324,325],{"class":98,"line":99},[96,326,327],{},"SELECT l.pid, l.mode, l.granted, l.classid, l.objid\n",[96,329,330],{"class":98,"line":105},[96,331,332],{},"FROM pg_locks l\n",[96,334,335],{"class":98,"line":112},[96,336,337],{},"WHERE l.locktype = 'advisory'\n",[96,339,340],{"class":98,"line":118},[96,341,342],{},"  AND l.pid = pg_backend_pid();\n",[16,344,345,348,349,352,353,355,356,358],{},[20,346,347],{},"Atteso:"," almeno una riga ",[25,350,351],{},"granted = true",". Il ",[25,354,235],{}," ",[20,357,31],{}," ha liberato il lock: è normale per la variante session-scoped.",[16,360,361,362,364,365,367,368,371,372,375,376,379,380,383,384,386,387,390],{},"Immagina ora ",[20,363,47],{}," (o un pool equivalente): dopo il ",[25,366,235],{}," la tua applicazione ",[20,369,370],{},"restituisce al pool"," il socket verso Postgres. Il ",[20,373,374],{},"processo backend"," (",[25,377,378],{},"pid"," sopra) spesso ",[20,381,382],{},"resta lo stesso"," per il prossimo client che ottiene quella connessione server. Il lock advisory è ancora attaccato a quel ",[25,385,378],{},": il prossimo worker che ottiene quel backend eredita un lock che ",[20,388,389],{},"non ha mai richiesto",". Sintomi: serializzazione “fantasma”, starvation, o due team diversi che condividono comportamenti impossibili da riprodurre in locale senza pool.",[16,392,393,399,400,402,403,406],{},[20,394,395,396,398],{},"Perché ",[25,397,181],{}," è la leva pratica:"," rilascia il lock a fine transazione, quindi il backend torna “pulito” per il prossimo affittuario del pool dopo ogni ",[25,401,235],{},"\u002F",[25,404,405],{},"ROLLBACK",", che è esattamente il ciclo di vita che transaction pooling assume.",[408,409,410],"blockquote",{},[16,411,412,415,416,419,420,422,423,426],{},[20,413,414],{},"Nota:"," in ",[20,417,418],{},"session pooling"," PgBouncer una connessione client mappa 1:1 su un backend per tutta la vita: il leak cross-client è meno frequente, ma resta il problema del lock dimenticato (",[25,421,281],{}," mancante) fino a disconnect. In ",[20,424,425],{},"transaction mode"," il cross-affitto del backend è il caso d’uso quotidiano.",[16,428,429,430,437],{},"Documentazione utile: ",[431,432,436],"a",{"href":433,"rel":434},"https:\u002F\u002Fwww.pgbouncer.org\u002Ffeatures.html",[435],"nofollow","PgBouncer features — pooling mode"," (transaction vs session).",[11,439,441,443],{"id":440},"for-update-sul-ledger-quando-lock-e-stato-devono-coincidere",[25,442,84],{}," sul ledger: quando lock e stato devono coincidere",[16,445,446,447,450,451,454],{},"Se la serializzazione deve essere ",[20,448,449],{},"la stessa transazione"," che aggiorna ",[25,452,453],{},"claim_count"," \u002F stato, spesso si usa una riga di ledger:",[87,456,458],{"className":89,"code":457,"language":91,"meta":92,"style":92},"BEGIN;\n\nSELECT job_key, claim_count\nFROM job_ledger\nWHERE job_key = 'report-daily'\nFOR UPDATE;\n\nUPDATE job_ledger\nSET claim_count = claim_count + 1, updated_at = now()\nWHERE job_key = 'report-daily';\n\nCOMMIT;\n",[25,459,460,464,468,473,478,483,488,492,497,502,507,512],{"__ignoreMap":92},[96,461,462],{"class":98,"line":99},[96,463,203],{},[96,465,466],{"class":98,"line":105},[96,467,109],{"emptyLinePlaceholder":108},[96,469,470],{"class":98,"line":112},[96,471,472],{},"SELECT job_key, claim_count\n",[96,474,475],{"class":98,"line":118},[96,476,477],{},"FROM job_ledger\n",[96,479,480],{"class":98,"line":124},[96,481,482],{},"WHERE job_key = 'report-daily'\n",[96,484,485],{"class":98,"line":130},[96,486,487],{},"FOR UPDATE;\n",[96,489,490],{"class":98,"line":136},[96,491,109],{"emptyLinePlaceholder":108},[96,493,494],{"class":98,"line":142},[96,495,496],{},"UPDATE job_ledger\n",[96,498,499],{"class":98,"line":147},[96,500,501],{},"SET claim_count = claim_count + 1, updated_at = now()\n",[96,503,504],{"class":98,"line":153},[96,505,506],{},"WHERE job_key = 'report-daily';\n",[96,508,510],{"class":98,"line":509},11,[96,511,109],{"emptyLinePlaceholder":108},[96,513,515],{"class":98,"line":514},12,[96,516,218],{},[16,518,519,520,523],{},"Qui il lock ",[20,521,522],{},"è"," la riga che stai mutando: niente “prima advisory, poi update” con una finestra concettuale in mezzo.",[11,525,527,529],{"id":526},"skip-locked-perché-non-è-la-serializzazione-che-cerchi",[25,528,53],{},": perché non è la serializzazione che cerchi",[16,531,532,534,535,538,539,542,543,546,547,550,551,554,555,558],{},[25,533,53],{}," serve a ",[20,536,537],{},"non attendere"," righe già bloccate: la sessione che legge ",[20,540,541],{},"prosegue subito"," senza quelle righe. Per un ",[20,544,545],{},"job con chiave nota"," che deve essere elaborato ",[20,548,549],{},"al più da un worker alla volta",", questo è spesso ",[20,552,553],{},"l’opposto"," di quello che vuoi: vuoi che il secondo worker ",[20,556,557],{},"aspetti",", non che “non veda” il lavoro.",[16,560,561],{},[20,562,193],{},[87,564,566],{"className":89,"code":565,"language":91,"meta":92,"style":92},"BEGIN;\nSELECT job_key FROM job_ledger WHERE job_key = 'report-daily' FOR UPDATE;\n-- non fare COMMIT ancora\n",[25,567,568,572,577],{"__ignoreMap":92},[96,569,570],{"class":98,"line":99},[96,571,203],{},[96,573,574],{"class":98,"line":105},[96,575,576],{},"SELECT job_key FROM job_ledger WHERE job_key = 'report-daily' FOR UPDATE;\n",[96,578,579],{"class":98,"line":112},[96,580,581],{},"-- non fare COMMIT ancora\n",[16,583,584,586],{},[20,585,223],{}," — prima senza skip:",[87,588,590],{"className":89,"code":589,"language":91,"meta":92,"style":92},"BEGIN;\nSELECT job_key FROM job_ledger WHERE job_key = 'report-daily' FOR UPDATE;\n",[25,591,592,596],{"__ignoreMap":92},[96,593,594],{"class":98,"line":99},[96,595,203],{},[96,597,598],{"class":98,"line":105},[96,599,576],{},[16,601,602,603,606],{},"Comportamento atteso: ",[20,604,605],{},"blocco"," finché A non committa. Serializzazione classica.",[16,608,609,610,612],{},"Fai ",[25,611,405],{}," su B, poi ripeti B con:",[87,614,616],{"className":89,"code":615,"language":91,"meta":92,"style":92},"BEGIN;\nSELECT job_key FROM job_ledger WHERE job_key = 'report-daily' FOR UPDATE SKIP LOCKED;\n",[25,617,618,622],{"__ignoreMap":92},[96,619,620],{"class":98,"line":99},[96,621,203],{},[96,623,624],{"class":98,"line":105},[96,625,626],{},"SELECT job_key FROM job_ledger WHERE job_key = 'report-daily' FOR UPDATE SKIP LOCKED;\n",[16,628,629,630,633,634,637,638,641,642,644],{},"Comportamento atteso: la query ",[20,631,632],{},"ritorna subito"," con ",[20,635,636],{},"zero righe"," (non c’è un’altra riga “libera” da restituire): il worker interpreta “nessun job” mentre in realtà il job ",[20,639,640],{},"c’è"," ed è solo bloccato. Per idempotenza su ",[20,643,296],{}," chiave, questo è la trappola.",[408,646,647],{},[16,648,649,650,653,654,657,658,661],{},"Lo escludiamo dal pattern “outbox \u002F claim pool” non per snobismo: lì vuoi proprio ",[20,651,652],{},"saltare"," righe già prese da altri worker su ",[20,655,656],{},"insieme"," di job; qui stiamo parlando di ",[20,659,660],{},"una chiave nota"," che deve essere mutuamente esclusa, non di “pesca la prossima riga libera”.",[11,663,665],{"id":664},"query-di-monitoring-pronta-advisory-in-attesa-oltre-n-secondi","Query di monitoring pronta (advisory in attesa oltre N secondi)",[16,667,668,669,672,673,675,676,679,680,683,684,63],{},"Obiettivo: alimentare un ",[20,670,671],{},"alert"," (“c’è un backend fermo da troppo tempo in attesa di advisory”). La query unisce ",[25,674,73],{}," (lock non concesso) a ",[25,677,678],{},"pg_stat_activity"," e, tramite le colonne identificative del lock, al ",[20,681,682],{},"bloccante"," che detiene il lock ",[25,685,686],{},"granted",[16,688,689,690,693],{},"Soglia di esempio: ",[20,691,692],{},"30 secondi"," — parametrica in produzione.",[87,695,697],{"className":89,"code":696,"language":91,"meta":92,"style":92},"SELECT\n  blocked_a.pid AS blocked_pid,\n  now() - blocked_a.query_start AS blocked_since,\n  blocked_a.state AS blocked_state,\n  blocked_a.wait_event_type,\n  blocked_a.wait_event,\n  left(blocked_a.query, 200) AS blocked_query_snippet,\n  blocking_a.pid AS blocking_pid,\n  now() - blocking_a.query_start AS blocking_query_age,\n  left(blocking_a.query, 200) AS blocking_query_snippet\nFROM pg_catalog.pg_locks blocked_l\nJOIN pg_catalog.pg_stat_activity blocked_a ON blocked_a.pid = blocked_l.pid\nJOIN pg_catalog.pg_locks blocking_l\n  ON blocking_l.locktype = blocked_l.locktype\n AND blocking_l.database IS NOT DISTINCT FROM blocked_l.database\n AND blocking_l.relation IS NOT DISTINCT FROM blocked_l.relation\n AND blocking_l.page IS NOT DISTINCT FROM blocked_l.page\n AND blocking_l.tuple IS NOT DISTINCT FROM blocked_l.tuple\n AND blocking_l.virtualxid IS NOT DISTINCT FROM blocked_l.virtualxid\n AND blocking_l.transactionid IS NOT DISTINCT FROM blocked_l.transactionid\n AND blocking_l.classid IS NOT DISTINCT FROM blocked_l.classid\n AND blocking_l.objid IS NOT DISTINCT FROM blocked_l.objid\n AND blocking_l.objsubid IS NOT DISTINCT FROM blocked_l.objsubid\n AND blocking_l.pid \u003C> blocked_l.pid\nJOIN pg_catalog.pg_stat_activity blocking_a ON blocking_a.pid = blocking_l.pid\nWHERE NOT blocked_l.granted\n  AND blocking_l.granted\n  AND blocked_l.locktype = 'advisory'\n  AND now() - blocked_a.query_start > interval '30 seconds';\n",[25,698,699,704,709,714,719,724,729,734,739,744,749,754,759,765,771,777,783,789,795,801,807,813,819,825,831,837,843,849,855],{"__ignoreMap":92},[96,700,701],{"class":98,"line":99},[96,702,703],{},"SELECT\n",[96,705,706],{"class":98,"line":105},[96,707,708],{},"  blocked_a.pid AS blocked_pid,\n",[96,710,711],{"class":98,"line":112},[96,712,713],{},"  now() - blocked_a.query_start AS blocked_since,\n",[96,715,716],{"class":98,"line":118},[96,717,718],{},"  blocked_a.state AS blocked_state,\n",[96,720,721],{"class":98,"line":124},[96,722,723],{},"  blocked_a.wait_event_type,\n",[96,725,726],{"class":98,"line":130},[96,727,728],{},"  blocked_a.wait_event,\n",[96,730,731],{"class":98,"line":136},[96,732,733],{},"  left(blocked_a.query, 200) AS blocked_query_snippet,\n",[96,735,736],{"class":98,"line":142},[96,737,738],{},"  blocking_a.pid AS blocking_pid,\n",[96,740,741],{"class":98,"line":147},[96,742,743],{},"  now() - blocking_a.query_start AS blocking_query_age,\n",[96,745,746],{"class":98,"line":153},[96,747,748],{},"  left(blocking_a.query, 200) AS blocking_query_snippet\n",[96,750,751],{"class":98,"line":509},[96,752,753],{},"FROM pg_catalog.pg_locks blocked_l\n",[96,755,756],{"class":98,"line":514},[96,757,758],{},"JOIN pg_catalog.pg_stat_activity blocked_a ON blocked_a.pid = blocked_l.pid\n",[96,760,762],{"class":98,"line":761},13,[96,763,764],{},"JOIN pg_catalog.pg_locks blocking_l\n",[96,766,768],{"class":98,"line":767},14,[96,769,770],{},"  ON blocking_l.locktype = blocked_l.locktype\n",[96,772,774],{"class":98,"line":773},15,[96,775,776],{}," AND blocking_l.database IS NOT DISTINCT FROM blocked_l.database\n",[96,778,780],{"class":98,"line":779},16,[96,781,782],{}," AND blocking_l.relation IS NOT DISTINCT FROM blocked_l.relation\n",[96,784,786],{"class":98,"line":785},17,[96,787,788],{}," AND blocking_l.page IS NOT DISTINCT FROM blocked_l.page\n",[96,790,792],{"class":98,"line":791},18,[96,793,794],{}," AND blocking_l.tuple IS NOT DISTINCT FROM blocked_l.tuple\n",[96,796,798],{"class":98,"line":797},19,[96,799,800],{}," AND blocking_l.virtualxid IS NOT DISTINCT FROM blocked_l.virtualxid\n",[96,802,804],{"class":98,"line":803},20,[96,805,806],{}," AND blocking_l.transactionid IS NOT DISTINCT FROM blocked_l.transactionid\n",[96,808,810],{"class":98,"line":809},21,[96,811,812],{}," AND blocking_l.classid IS NOT DISTINCT FROM blocked_l.classid\n",[96,814,816],{"class":98,"line":815},22,[96,817,818],{}," AND blocking_l.objid IS NOT DISTINCT FROM blocked_l.objid\n",[96,820,822],{"class":98,"line":821},23,[96,823,824],{}," AND blocking_l.objsubid IS NOT DISTINCT FROM blocked_l.objsubid\n",[96,826,828],{"class":98,"line":827},24,[96,829,830],{}," AND blocking_l.pid \u003C> blocked_l.pid\n",[96,832,834],{"class":98,"line":833},25,[96,835,836],{},"JOIN pg_catalog.pg_stat_activity blocking_a ON blocking_a.pid = blocking_l.pid\n",[96,838,840],{"class":98,"line":839},26,[96,841,842],{},"WHERE NOT blocked_l.granted\n",[96,844,846],{"class":98,"line":845},27,[96,847,848],{},"  AND blocking_l.granted\n",[96,850,852],{"class":98,"line":851},28,[96,853,854],{},"  AND blocked_l.locktype = 'advisory'\n",[96,856,858],{"class":98,"line":857},29,[96,859,860],{},"  AND now() - blocked_a.query_start > interval '30 seconds';\n",[16,862,863],{},[20,864,865],{},"Cosa guardare per colonna (per tuning alert):",[867,868,869,878,886,916,924,932],"ul",{},[870,871,872,877],"li",{},[20,873,874],{},[25,875,876],{},"blocked_pid",": backend Postgres del worker in attesa.",[870,879,880,885],{},[20,881,882],{},[25,883,884],{},"blocked_since",": da quanto quella query è in esecuzione — utile come proxy della durata dell’attesa sul lock.",[870,887,888,900,901,904,905,908,909,893,912,915],{},[20,889,890,893,894,893,897],{},[25,891,892],{},"blocked_state"," \u002F ",[25,895,896],{},"wait_event_type",[25,898,899],{},"wait_event",": in attesa su lock tipicamente ",[25,902,903],{},"active"," + ",[25,906,907],{},"Lock"," + evento ",[25,910,911],{},"advisory",[25,913,914],{},"transactionid"," a seconda del piano; utile per distinguere attese su lock da I\u002FO.",[870,917,918,923],{},[20,919,920],{},[25,921,922],{},"blocked_query_snippet",": testo troncato della query in esecuzione (attenzione a PII nei log).",[870,925,926,931],{},[20,927,928],{},[25,929,930],{},"blocking_pid",": chi detiene il lock advisory concesso.",[870,933,934,939],{},[20,935,936],{},[25,937,938],{},"blocking_query_snippet",": cosa sta facendo il detentore — spesso è la transazione lunga da ottimizzare o da limitare nel timeout.",[16,941,942,943,946],{},"In ambienti molto rumorosi filtra ulteriormente su ",[25,944,945],{},"classid = 7742"," se usi solo quella classe per i job.",[11,948,950],{"id":949},"cosa-resta-fuori","Cosa resta fuori",[16,952,953,954,957,958,633,961,963],{},"Patroni, fail-over, ORM, ",[25,955,956],{},"LISTEN\u002FNOTIFY",", pattern ",[20,959,960],{},"outbox",[25,962,53],{}," su tabella di claim — sono pezzi giusti ma altre storie.",[11,965,967],{"id":966},"chiusura","Chiusura",[16,969,970,971,973,974,980,981,984,985,987,988,991],{},"La domanda utile non è “advisory o ",[25,972,84],{},"?” da tabella astratta, ma: ",[20,975,976,977,979],{},"il mio lock sopravvive al ",[25,978,235],{}," in modo che il pool lo regali al prossimo client?"," e ",[20,982,983],{},"il mio worker interpreta “nessuna riga” come “nessun lavoro” quando in realtà il lavoro è solo bloccato?"," Se porti in produzione ",[25,986,261],{}," con transaction pooling senza esserne consapevole, il bug è ",[20,989,990],{},"silenzioso"," finché non misuri contention o incroci casuali tra tenant\u002Fjob.",[11,993,995],{"id":994},"riferimenti","Riferimenti",[867,997,998,1005,1016,1031],{},[870,999,1000],{},[431,1001,1004],{"href":1002,"rel":1003},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fexplicit-locking.html#ADVISORY-LOCKS",[435],"Advisory locks",[870,1006,1007,1008],{},"Funzioni: ",[431,1009,1012,893,1014],{"href":1010,"rel":1011},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Ffunctions-admin.html#FUNCTIONS-ADVISORY-LOCKS",[435],[25,1013,261],{},[25,1015,181],{},[870,1017,1018,1024,1025],{},[431,1019,1022],{"href":1020,"rel":1021},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fview-pg-locks.html",[435],[25,1023,73],{},", ",[431,1026,1029],{"href":1027,"rel":1028},"https:\u002F\u002Fwww.postgresql.org\u002Fdocs\u002Fcurrent\u002Fmonitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW",[435],[25,1030,678],{},[870,1032,1033],{},[431,1034,1036],{"href":433,"rel":1035},[435],"PgBouncer pooling modes",[1038,1039,1040],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":92,"searchDepth":105,"depth":105,"links":1042},[1043,1044,1045,1047,1052,1054,1056,1057,1058,1059],{"id":13,"depth":105,"text":14},{"id":77,"depth":105,"text":78},{"id":177,"depth":105,"text":1046},"Dimostrazione breve: pg_advisory_xact_lock fa quello che ti aspetti",{"id":257,"depth":105,"text":1048,"children":1049},"Il caso che brucia: pg_advisory_lock e il pool in transaction mode",[1050],{"id":286,"depth":112,"text":1051},"Riproduzione senza PgBouncer (stesso backend, una sola psql)",{"id":440,"depth":105,"text":1053},"FOR UPDATE sul ledger: quando lock e stato devono coincidere",{"id":526,"depth":105,"text":1055},"SKIP LOCKED: perché non è la serializzazione che cerchi",{"id":664,"depth":105,"text":665},{"id":949,"depth":105,"text":950},{"id":966,"depth":105,"text":967},{"id":994,"depth":105,"text":995},"\u002Fblog\u002F31_postgres_advisory_lock_connection_pool_leak_1x.webp","Perché un pg_advisory_lock di sessione sopravvive al COMMIT e rompe i pool in transaction mode, come riprodurlo, e cosa usare al posto di SKIP LOCKED.",false,"md","it",{},"\u002Fit\u002Fblog\u002Fpostgres-advisory-lock-connection-pool-leak","2026-06-13",{"title":5,"description":1061},"Advisory lock Postgres e leak nel pool","postgres-advisory-lock-connection-pool-leak","it\u002Fblog\u002Fpostgres-advisory-lock-connection-pool-leak",[1073,1074,1075],"postgres","mvp","automation","a7WzrEuJ5Y1i93KVcOIVeX-W5L3bqwqwUy0GtlAb-Ks"]