Class: Portless::RouteStore

Inherits:
Object
  • Object
show all
Defined in:
lib/portless/route_store.rb

Overview

The on-disk routing table (routes.json): host → backend port → owning pid. No daemon API — apps register/deregister by editing this file under a directory mutex (atomic mkdir), and the proxy watches it. Dead-pid entries are reaped on every load. Mirrors portless's RouteStore.

Defined Under Namespace

Classes: Route

Constant Summary collapse

LOCK_STALE_SECONDS =
10
LOCK_BUDGET_SECONDS =
5

Instance Method Summary collapse

Constructor Details

#initialize(file: State.routes_file, lock: State.routes_lock) ⇒ RouteStore

Returns a new instance of RouteStore.



19
20
21
22
# File 'lib/portless/route_store.rb', line 19

def initialize(file: State.routes_file, lock: State.routes_lock)
  @file = file
  @lock = lock
end

Instance Method Details

#add(hostname:, port:, pid:, force: false, tailscale: nil, ngrok: nil) ⇒ Object

Register (or replace) a route. Conflicts with a live different owner raise unless force, which SIGTERMs the incumbent. Alias routes use pid 0. Public share URLs (tailscale/ngrok), when present, are recorded so list can show them while the run is active.



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/portless/route_store.rb', line 35

def add(hostname:, port:, pid:, force: false, tailscale: nil, ngrok: nil)
  with_lock do
    all = load.reject { |r| dead?(r["pid"]) }
    existing = all.find { |r| r["hostname"] == hostname }
    if existing && existing["pid"].to_i != pid.to_i && !dead?(existing["pid"])
      unless force
        raise RouteConflictError,
              "#{hostname} is already served by pid #{existing['pid']} — pass --force to take it over"
      end
      terminate(existing["pid"])
    end
    all.reject! { |r| r["hostname"] == hostname }
    entry = { "hostname" => hostname, "port" => port, "pid" => pid }
    entry["tailscale"] = tailscale if tailscale
    entry["ngrok"] = ngrok if ngrok
    all << entry
    write(all)
  end
end

#pruneObject

Drop dead-pid routes and return them (so callers can reap the orphaned process still holding the backend port). Alias routes (pid 0) are kept.



71
72
73
74
75
76
77
# File 'lib/portless/route_store.rb', line 71

def prune
  with_lock do
    dead, alive = load.partition { |r| dead?(r["pid"]) }
    write(alive)
    dead.map { |r| Route.new(hostname: r["hostname"], port: r["port"], pid: r["pid"]) }
  end
end

#remove(hostname, owner_pid: nil) ⇒ Object

Remove a route only if still owned by owner_pid (so a force-replaced predecessor doesn't delete the successor's route on its way out). Returns whether anything was actually removed.



58
59
60
61
62
63
64
65
66
67
# File 'lib/portless/route_store.rb', line 58

def remove(hostname, owner_pid: nil)
  with_lock do
    before = load
    after = before.reject do |r|
      r["hostname"] == hostname && (owner_pid.nil? || r["pid"].to_i == owner_pid.to_i)
    end
    write(after)
    after.size < before.size
  end
end

#routesObject



24
25
26
27
28
29
# File 'lib/portless/route_store.rb', line 24

def routes
  load.map do |h|
    Route.new(hostname: h["hostname"], port: h["port"], pid: h["pid"],
              tailscale: h["tailscale"], ngrok: h["ngrok"])
  end
end