forked from retoor/devplacepy
Fix a tunnel-hostname race that could publish duplicate rows
tunnels.create() checked for an existing row by hostname and then inserted/updated in a separate statement, with no lock between the two. Concurrent publish calls for the same port (e.g. the editor's Ports extension re-POSTing on every onDidChangeTunnels event) could race past the check together and each insert their own row for the identical hostname. Since routers/tunnel.py resolves a tunnel by a plain find_one(hostname=...) with no ordering, whichever duplicate it happened to return decided whether a request reached the app or got a generic "no tunnel is published at this address" 404 - even while a sibling row for the same hostname was active and serving traffic. Replaced the check-then-write with a single atomic INSERT ... ON CONFLICT(hostname) DO UPDATE, which needs hostname to actually be unique: idx_tunnels_hostname is now a UNIQUE index instead of a plain one. Since existing databases likely already carry duplicate rows from this race, init_db() now runs a one-time _dedupe_tunnel_hostnames() pass (keeps the best row per hostname - active > provisioning > pending > failed, then newest) before dropping and recreating the index as unique - CREATE UNIQUE INDEX IF NOT EXISTS silently no-ops when an index with that name already exists, so the old non-unique index has to be dropped first or the constraint would never actually upgrade. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,54 @@ def migrate_bug_tables_to_issue_tables() -> None:
|
||||
logger.info("Dropped table %s after migration", source_name)
|
||||
|
||||
|
||||
_TUNNEL_STATUS_RANK = {
|
||||
"active": 4,
|
||||
"provisioning": 3,
|
||||
"pending": 2,
|
||||
"failed": 1,
|
||||
"suspended": 0,
|
||||
}
|
||||
|
||||
|
||||
def _dedupe_tunnel_hostnames() -> None:
|
||||
if "tunnels" not in db.tables:
|
||||
return
|
||||
table = get_table("tunnels")
|
||||
if not table.has_column("hostname") or not table.has_column("uid"):
|
||||
return
|
||||
groups = list(
|
||||
db.query(
|
||||
"SELECT hostname FROM tunnels "
|
||||
"WHERE hostname IS NOT NULL AND hostname != '' "
|
||||
"GROUP BY hostname HAVING COUNT(*) > 1"
|
||||
)
|
||||
)
|
||||
for group in groups:
|
||||
hostname = group["hostname"]
|
||||
dupes = list(table.find(hostname=hostname))
|
||||
dupes.sort(
|
||||
key=lambda r: (
|
||||
_TUNNEL_STATUS_RANK.get(r.get("status") or "", -1),
|
||||
r.get("deleted_at") is None,
|
||||
r.get("created_at") or "",
|
||||
r.get("id") or 0,
|
||||
),
|
||||
reverse=True,
|
||||
)
|
||||
losers = [r["uid"] for r in dupes[1:]]
|
||||
if not losers:
|
||||
continue
|
||||
with db:
|
||||
for uid in losers:
|
||||
db.query("DELETE FROM tunnels WHERE uid = :uid", uid=uid)
|
||||
logger.warning(
|
||||
"Removed %d duplicate tunnel row(s) for hostname %s, kept %s",
|
||||
len(losers),
|
||||
hostname,
|
||||
dupes[0]["uid"],
|
||||
)
|
||||
|
||||
|
||||
def init_db():
|
||||
tables = db.tables
|
||||
_index(db, "users", "idx_users_username", ["username"])
|
||||
@@ -785,7 +833,9 @@ def init_db():
|
||||
_index(db, "instances", "idx_instances_workspace", ["is_workspace", "status"])
|
||||
_index(db, "instances", "idx_instances_workspace_owner", ["workspace_owner_uid"])
|
||||
_index(db, "instances", "idx_instances_tunnel_name", ["tunnel_name"])
|
||||
_index(db, "tunnels", "idx_tunnels_hostname", ["hostname"])
|
||||
_dedupe_tunnel_hostnames()
|
||||
_drop_index(db, "idx_tunnels_hostname")
|
||||
_index(db, "tunnels", "idx_tunnels_hostname", ["hostname"], unique=True)
|
||||
_index(db, "tunnels", "idx_tunnels_instance", ["instance_uid"])
|
||||
_index(db, "tunnels", "idx_tunnels_user", ["user_uid"])
|
||||
_index(db, "tunnels", "idx_tunnels_state", ["desired_state", "status"])
|
||||
|
||||
Reference in New Issue
Block a user