yamine 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/yamine/agent.rb +43 -0
- data/lib/yamine/cli/boot.rb +50 -5
- data/lib/yamine/cli/routes.rb +72 -5
- data/lib/yamine/errors.rb +23 -4
- data/lib/yamine/proxy_control.rb +2 -2
- data/lib/yamine/route_store.rb +51 -20
- data/lib/yamine/runner.rb +6 -3
- data/lib/yamine/version.rb +1 -1
- data/lib/yamine.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de59fb684553be7e33ba1a9ea4688bb075a3147ae6225cdc634818c869900163
|
|
4
|
+
data.tar.gz: 920ac5409e8e0803ed3e3d8ba01663ea03f1543db30e4921585ef6bfdf0d33be
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8f0cb59209724dd41946c4e0e82c6e2aa01ac9e49575bf461610ed525d98d50fb1566260cf83f7a0b50e19cffd4ab9d6658e372d1c094529e12276e256541161
|
|
7
|
+
data.tar.gz: 8bb784a546f91a27c5a9b00584fbe14560022d655acd861d5bfdba13db8f187b70913fd129b46b73948068d17c1affca556fd0eb4a67690074d74dbcfdb96214
|
data/lib/yamine/agent.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Yamine
|
|
4
|
+
# Agent identity for multi-agent codebases.
|
|
5
|
+
#
|
|
6
|
+
# Every route records which agent booted it so concurrent agents can
|
|
7
|
+
# coexist: list shows owners, conflicts name the other agent, quotas
|
|
8
|
+
# are enforced per agent, and stop/prune can target one agent without
|
|
9
|
+
# touching another's live backends. Identity comes from YAMINE_AGENT
|
|
10
|
+
# (set by the harness per session); without it we fall back to
|
|
11
|
+
# USER@HOST so humans sharing a machine still get distinct owners.
|
|
12
|
+
module Agent
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def name
|
|
16
|
+
env = ENV["YAMINE_AGENT"]
|
|
17
|
+
return env.strip unless env.nil? || env.strip.empty?
|
|
18
|
+
|
|
19
|
+
user = ENV["USER"] || ENV["LOGNAME"] || "unknown"
|
|
20
|
+
host = begin
|
|
21
|
+
require "socket"
|
|
22
|
+
Socket.gethostname
|
|
23
|
+
rescue StandardError
|
|
24
|
+
"localhost"
|
|
25
|
+
end
|
|
26
|
+
"#{user}@#{host}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Default per-agent route cap. Generous: N agents x M services must
|
|
30
|
+
# fit in the ephemeral port range with headroom. Overridable via
|
|
31
|
+
# YAMINE_MAX_ROUTES (0 disables the cap).
|
|
32
|
+
DEFAULT_MAX_ROUTES = 32
|
|
33
|
+
|
|
34
|
+
def max_routes
|
|
35
|
+
raw = ENV["YAMINE_MAX_ROUTES"]
|
|
36
|
+
return DEFAULT_MAX_ROUTES if raw.nil? || raw.strip.empty?
|
|
37
|
+
|
|
38
|
+
Integer(raw)
|
|
39
|
+
rescue ArgumentError
|
|
40
|
+
DEFAULT_MAX_ROUTES
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/yamine/cli/boot.rb
CHANGED
|
@@ -18,6 +18,9 @@ module Yamine
|
|
|
18
18
|
variant = ENV["YAMINE_VARIANT"]
|
|
19
19
|
opts = ctx.parse_flags(args, %i[variant tld force])
|
|
20
20
|
resolved = resolve!(ctx, variant: opts[:variant] || variant, tld: opts[:tld])
|
|
21
|
+
# Ownership gate before any side effects: no proxy spawn, no
|
|
22
|
+
# port allocation when we'd refuse anyway.
|
|
23
|
+
check_worktree_ownership!(ctx, resolved, force: opts[:force])
|
|
21
24
|
ensure_proxy!(ctx)
|
|
22
25
|
boot_all(ctx, resolved, opts)
|
|
23
26
|
end
|
|
@@ -60,9 +63,11 @@ module Yamine
|
|
|
60
63
|
puts "--"
|
|
61
64
|
|
|
62
65
|
processes.each do |proc_name, entry|
|
|
63
|
-
next if entry["proxy"] == false
|
|
64
|
-
|
|
65
66
|
hostname = Resolver.hostname_for(resolved, proc_name)
|
|
67
|
+
if entry["proxy"] == false
|
|
68
|
+
boot_background(ctx, runner, resolved, proc_name, entry, opts, children)
|
|
69
|
+
next
|
|
70
|
+
end
|
|
66
71
|
next unless hostname
|
|
67
72
|
|
|
68
73
|
url = Hostname.url(hostname, port: ctx.proxy_port, tls: ctx.proxy_tls)
|
|
@@ -101,9 +106,49 @@ module Yamine
|
|
|
101
106
|
ctx.report_unresolved(routes_registered.flat_map { |r| r[:hostnames] })
|
|
102
107
|
|
|
103
108
|
# Supervisor: exit when ANY child dies (loud cleanup).
|
|
104
|
-
all_pids = routes_registered.map { |r| r[:app].pid }
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
all_pids = routes_registered.map { |r| r[:app].pid } + children.map { |c| c[:pid] }
|
|
110
|
+
all_hostnames = routes_registered.flat_map { |r| r[:hostnames] }
|
|
111
|
+
trap_cleanup(ctx, all_hostnames, all_pids)
|
|
112
|
+
supervise_tree(ctx, all_hostnames, all_pids)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# A background process (proxy: false) is spawned, logged, and
|
|
116
|
+
# supervised exactly like an HTTP one — it just gets no route.
|
|
117
|
+
def boot_background(ctx, runner, resolved, proc_name, entry, opts, children)
|
|
118
|
+
cmd = entry["cmd"].to_s
|
|
119
|
+
if cmd.match?(Yamine::Procfile::COMPOUND)
|
|
120
|
+
$stderr.puts " [#{proc_name}] ERROR: compound line (&&, ||, |, ;) — run explicitly: yamine run -- #{cmd}"
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
port = opts[:app_port] || Ports.find_free
|
|
124
|
+
url = "background://#{resolved.app}/#{proc_name}"
|
|
125
|
+
app = runner.boot_run(name: proc_name, hostname: "#{resolved.app}.#{proc_name}.internal",
|
|
126
|
+
url: url, dir: Dir.pwd, command: ["sh", "-c", cmd], port: port,
|
|
127
|
+
force: opts[:force], rails_dev_host: nil, register: false)
|
|
128
|
+
children << { name: proc_name, pid: app.pid }
|
|
129
|
+
puts " [#{proc_name}] background (pid #{app.pid})"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Refuse to boot over another agent's live routes unless forced.
|
|
133
|
+
# Compares our worktree dir (spec.dir) against the owner's: same
|
|
134
|
+
# dir + same agent is a restart, anything else names the owner.
|
|
135
|
+
def check_worktree_ownership!(ctx, resolved, force:)
|
|
136
|
+
mine = Agent.name
|
|
137
|
+
here = File.expand_path(Dir.pwd)
|
|
138
|
+
Resolver.hostnames(resolved).each do |hostname|
|
|
139
|
+
entry = ctx.store.find(hostname)
|
|
140
|
+
next unless entry
|
|
141
|
+
next unless entry["pid"] != 0 && ProxyControl.pid_alive?(entry["pid"])
|
|
142
|
+
next if entry["agent"] == mine && entry.dig("spec", "dir") == here
|
|
143
|
+
next if force
|
|
144
|
+
|
|
145
|
+
owner = entry["agent"] && !entry["agent"].empty? ? "agent #{entry["agent"].inspect}" : "PID #{entry["pid"]}"
|
|
146
|
+
dir = entry.dig("spec", "dir")
|
|
147
|
+
$stderr.puts "Error: #{hostname} is live and owned by #{owner}#{dir ? " (#{dir})" : ""}."
|
|
148
|
+
$stderr.puts " Work in your own worktree (each branch gets its own URL), or take over explicitly:"
|
|
149
|
+
$stderr.puts " yamine start --force"
|
|
150
|
+
exit 1
|
|
151
|
+
end
|
|
107
152
|
end
|
|
108
153
|
|
|
109
154
|
def build_env(ctx, resolved, entry)
|
data/lib/yamine/cli/routes.rb
CHANGED
|
@@ -8,12 +8,16 @@ module Yamine
|
|
|
8
8
|
module_function
|
|
9
9
|
|
|
10
10
|
# yamine get <name> [--service x] [--variant y] [--tld z]
|
|
11
|
+
# yamine get --all [--json]
|
|
11
12
|
#
|
|
12
13
|
# Variant and TLDs are inherited from the CURRENT directory's
|
|
13
14
|
# context (worktree branch, YAMINE_* env, config) so cross-service
|
|
14
15
|
# wiring works inside a variant: from a fix-ui worktree,
|
|
15
16
|
# `get backend` -> https://fix-ui.backend.localhost.
|
|
16
17
|
def get(ctx, args)
|
|
18
|
+
if args.delete("--all")
|
|
19
|
+
return registry(ctx, args)
|
|
20
|
+
end
|
|
17
21
|
name = args.first
|
|
18
22
|
raise Error, "Usage: yamine get <name> [--service s] [--variant v] [--tld t]" unless name
|
|
19
23
|
|
|
@@ -36,7 +40,8 @@ module Yamine
|
|
|
36
40
|
{ hostname: r["hostname"],
|
|
37
41
|
url: Hostname.url(r["hostname"], port: port, tls: tls),
|
|
38
42
|
target: r["target"], kind: r["kind"],
|
|
39
|
-
pid: r["pid"],
|
|
43
|
+
pid: r["pid"], agent: r["agent"],
|
|
44
|
+
supervised: !r["spec"].nil?,
|
|
40
45
|
alive: alive_state(ctx, r) }
|
|
41
46
|
end
|
|
42
47
|
if json
|
|
@@ -56,6 +61,34 @@ module Yamine
|
|
|
56
61
|
puts
|
|
57
62
|
end
|
|
58
63
|
|
|
64
|
+
# Shared discovery: `get --all` lists every live route (any owner)
|
|
65
|
+
# with its URL and agent, so one agent can find another's services
|
|
66
|
+
# without coupling. `--json` emits the same stable keys as list.
|
|
67
|
+
def registry(ctx, args)
|
|
68
|
+
json = args.delete("--json")
|
|
69
|
+
routes = ctx.store.load_routes
|
|
70
|
+
port = ctx.proxy_port
|
|
71
|
+
tls = ctx.proxy_tls
|
|
72
|
+
entries = routes.map do |r|
|
|
73
|
+
{ hostname: r["hostname"],
|
|
74
|
+
url: Hostname.url(r["hostname"], port: port, tls: tls),
|
|
75
|
+
agent: r["agent"], alive: alive_state(ctx, r) }
|
|
76
|
+
end
|
|
77
|
+
if json
|
|
78
|
+
require "json"
|
|
79
|
+
puts JSON.generate({ routes: entries })
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
if entries.empty?
|
|
83
|
+
puts "No active routes."
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
entries.each do |e|
|
|
87
|
+
owner = e[:agent] ? " (#{e[:agent]})" : ""
|
|
88
|
+
puts " #{e[:url]}#{owner}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
59
92
|
def alive_state(ctx, route)
|
|
60
93
|
if route["pid"] == 0
|
|
61
94
|
ctx.backend_alive?(route) ? "reachable" : "unreachable"
|
|
@@ -69,10 +102,11 @@ module Yamine
|
|
|
69
102
|
end
|
|
70
103
|
|
|
71
104
|
def label_for(entry)
|
|
105
|
+
owner = entry[:agent] ? " #{entry[:agent]}" : ""
|
|
72
106
|
if entry[:pid] == 0
|
|
73
|
-
"(alias, #{entry[:alive]})"
|
|
107
|
+
"(alias, #{entry[:alive]}#{owner})"
|
|
74
108
|
else
|
|
75
|
-
"(pid #{entry[:pid]}, #{entry[:alive]})"
|
|
109
|
+
"(pid #{entry[:pid]}, #{entry[:alive]}#{owner})"
|
|
76
110
|
end
|
|
77
111
|
end
|
|
78
112
|
|
|
@@ -84,7 +118,19 @@ module Yamine
|
|
|
84
118
|
label_for(entry)
|
|
85
119
|
end
|
|
86
120
|
|
|
87
|
-
def prune(ctx,
|
|
121
|
+
def prune(ctx, args)
|
|
122
|
+
if (i = args.index("--agent"))
|
|
123
|
+
agent = args.fetch(i + 1, nil)
|
|
124
|
+
raise Error, "Usage: yamine prune --agent NAME" if agent.nil? || agent.empty?
|
|
125
|
+
|
|
126
|
+
stale = ctx.store.prune_stale(agent: agent)
|
|
127
|
+
if stale.empty?
|
|
128
|
+
puts "No stale routes for agent #{agent.inspect}."
|
|
129
|
+
else
|
|
130
|
+
stale.each { |r| puts "Removed stale route #{r["hostname"]}." }
|
|
131
|
+
end
|
|
132
|
+
return
|
|
133
|
+
end
|
|
88
134
|
stale = ctx.store.prune_stale
|
|
89
135
|
if stale.empty?
|
|
90
136
|
puts "No stale routes."
|
|
@@ -124,15 +170,31 @@ module Yamine
|
|
|
124
170
|
# Stop the app in the current directory (route + backend).
|
|
125
171
|
# Exit codes are machine-readable for agents: 0 stopped something,
|
|
126
172
|
# 2 no route here, 3 route existed but the backend was already gone.
|
|
127
|
-
|
|
173
|
+
# Only our own agent's routes are touched: a foreign-owned live
|
|
174
|
+
# route is reported, never killed (shared backends survive one
|
|
175
|
+
# agent's cleanup). --force overrides and names the previous owner.
|
|
176
|
+
def stop(ctx, args, out: $stdout)
|
|
177
|
+
force = args.delete("--force")
|
|
128
178
|
resolved = Resolver.resolve(Dir.pwd)
|
|
129
179
|
hostnames = Resolver.hostnames(resolved)
|
|
180
|
+
mine = Agent.name
|
|
130
181
|
stopped = []
|
|
131
182
|
gone = []
|
|
183
|
+
foreign = []
|
|
132
184
|
hostnames.each do |hostname|
|
|
133
185
|
entry = ctx.store.find(hostname)
|
|
134
186
|
next unless entry
|
|
135
187
|
|
|
188
|
+
if entry["pid"] != 0 && entry["agent"] && entry["agent"] != mine &&
|
|
189
|
+
ProxyControl.pid_alive?(entry["pid"])
|
|
190
|
+
unless force
|
|
191
|
+
owner = entry["agent"].empty? ? "PID #{entry["pid"]}" : "agent #{entry["agent"].inspect}"
|
|
192
|
+
foreign << "#{hostname} (owned by #{owner})"
|
|
193
|
+
next
|
|
194
|
+
end
|
|
195
|
+
out.puts "Taking over #{hostname} from #{entry["agent"].inspect} (--force)."
|
|
196
|
+
end
|
|
197
|
+
|
|
136
198
|
backend_pid = ctx.backend_pid_for(entry)
|
|
137
199
|
if backend_pid && ProxyControl.pid_alive?(backend_pid)
|
|
138
200
|
begin
|
|
@@ -155,6 +217,11 @@ module Yamine
|
|
|
155
217
|
stopped.each { |s| out.puts "Stopped #{s}." }
|
|
156
218
|
return 0
|
|
157
219
|
end
|
|
220
|
+
if foreign.any?
|
|
221
|
+
out.puts "Skipped live routes owned by other agents: #{foreign.join(", ")}."
|
|
222
|
+
out.puts "Take over explicitly with `yamine stop --force`."
|
|
223
|
+
return 4
|
|
224
|
+
end
|
|
158
225
|
if gone.any?
|
|
159
226
|
out.puts "Route existed but the backend was already gone: #{gone.join(", ")}."
|
|
160
227
|
return 3
|
data/lib/yamine/errors.rb
CHANGED
|
@@ -4,13 +4,32 @@ module Yamine
|
|
|
4
4
|
class Error < StandardError; end
|
|
5
5
|
class ConfigError < Error; end
|
|
6
6
|
class RouteConflictError < Error
|
|
7
|
-
attr_reader :hostname, :existing_pid
|
|
7
|
+
attr_reader :hostname, :existing_pid, :existing_agent, :existing_dir
|
|
8
8
|
|
|
9
|
-
def initialize(hostname, existing_pid)
|
|
9
|
+
def initialize(hostname, existing_pid, existing_agent: nil, existing_dir: nil)
|
|
10
10
|
@hostname = hostname
|
|
11
11
|
@existing_pid = existing_pid
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
@existing_agent = existing_agent
|
|
13
|
+
@existing_dir = existing_dir
|
|
14
|
+
owner = if existing_agent && !existing_agent.empty?
|
|
15
|
+
"agent #{existing_agent.inspect}#{existing_dir ? " (#{existing_dir})" : ""}"
|
|
16
|
+
else
|
|
17
|
+
"PID #{existing_pid}"
|
|
18
|
+
end
|
|
19
|
+
super("\"#{hostname}\" is already registered by a running #{owner}. " \
|
|
20
|
+
"Use --force to override.")
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
class QuotaExceededError < Error
|
|
24
|
+
attr_reader :agent, :limit, :count
|
|
25
|
+
|
|
26
|
+
def initialize(agent, limit, count)
|
|
27
|
+
@agent = agent
|
|
28
|
+
@limit = limit
|
|
29
|
+
@count = count
|
|
30
|
+
super("Agent #{agent.inspect} already owns #{count} routes (limit #{limit}). " \
|
|
31
|
+
"Stop something first: `yamine stop`, or prune one agent's orphans " \
|
|
32
|
+
"with `yamine prune --agent NAME`.")
|
|
14
33
|
end
|
|
15
34
|
end
|
|
16
35
|
class ProxyNotRunningError < Error; end
|
data/lib/yamine/proxy_control.rb
CHANGED
|
@@ -16,11 +16,11 @@ module Yamine
|
|
|
16
16
|
module_function
|
|
17
17
|
|
|
18
18
|
# Path to the yamine executable relative to this file
|
|
19
|
-
# (lib/
|
|
19
|
+
# (lib/yamine -> gem root/bin). Used for daemon spawn and
|
|
20
20
|
# service install; both must resolve identically in dev checkouts
|
|
21
21
|
# and installed gems.
|
|
22
22
|
def bin_path
|
|
23
|
-
File.expand_path("
|
|
23
|
+
File.expand_path("../../bin/yamine", __dir__)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def root?
|
data/lib/yamine/route_store.rb
CHANGED
|
@@ -4,12 +4,14 @@ require "fileutils"
|
|
|
4
4
|
require "json"
|
|
5
5
|
|
|
6
6
|
module Yamine
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
# Persistent hostname -> backend mapping in ~/.yamine/routes.json.
|
|
8
|
+
#
|
|
9
|
+
# Entries: {hostname, target, kind, pid, agent} where target is a unix
|
|
10
|
+
# socket path (kind "socket") or "127.0.0.1:PORT" (kind "tcp"), pid 0
|
|
11
|
+
# marks a static alias, and agent names the owner (YAMINE_AGENT or
|
|
12
|
+
# USER@HOST fallback) so concurrent agents can coexist. Guarded by
|
|
13
|
+
# flock; stale PIDs pruned on read.
|
|
14
|
+
# Modeled on portless RouteStore and puma-dev's AppPool registry.
|
|
13
15
|
class RouteStore
|
|
14
16
|
FILE_MODE = 0o644
|
|
15
17
|
DIR_MODE = 0o755
|
|
@@ -71,13 +73,22 @@ module Yamine
|
|
|
71
73
|
# Returns killed pid when force takes over a live route.
|
|
72
74
|
# `spec` marks daemon-supervised managed apps ({dir: ...}); the
|
|
73
75
|
# proxy supervisor may idle-kill and boot-on-request those backends.
|
|
74
|
-
|
|
76
|
+
# `agent` records the owner (defaults to Agent.name); foreign-owned
|
|
77
|
+
# live routes can only be taken with force: true, and the conflict
|
|
78
|
+
# error names the other agent plus its worktree dir.
|
|
79
|
+
def add_route(hostname, target, pid, kind:, force: false, spec: nil, agent: nil)
|
|
80
|
+
agent ||= Agent.name
|
|
75
81
|
killed = nil
|
|
76
82
|
with_lock do
|
|
77
83
|
routes = load_routes(prune: true)
|
|
84
|
+
enforce_quota!(routes, agent, hostname)
|
|
78
85
|
existing = routes.find { |r| r["hostname"] == hostname }
|
|
79
86
|
if existing && existing["pid"] != pid && alive?(existing["pid"])
|
|
80
|
-
|
|
87
|
+
unless force
|
|
88
|
+
raise RouteConflictError.new(hostname, existing["pid"],
|
|
89
|
+
existing_agent: existing["agent"],
|
|
90
|
+
existing_dir: existing.dig("spec", "dir"))
|
|
91
|
+
end
|
|
81
92
|
|
|
82
93
|
begin
|
|
83
94
|
Process.kill("TERM", existing["pid"])
|
|
@@ -87,7 +98,8 @@ module Yamine
|
|
|
87
98
|
end
|
|
88
99
|
end
|
|
89
100
|
routes.reject! { |r| r["hostname"] == hostname }
|
|
90
|
-
entry = { "hostname" => hostname, "target" => target, "kind" => kind,
|
|
101
|
+
entry = { "hostname" => hostname, "target" => target, "kind" => kind,
|
|
102
|
+
"pid" => pid, "agent" => agent }
|
|
91
103
|
entry["spec"] = spec if spec
|
|
92
104
|
routes << entry
|
|
93
105
|
save_routes(routes)
|
|
@@ -95,33 +107,52 @@ module Yamine
|
|
|
95
107
|
killed
|
|
96
108
|
end
|
|
97
109
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
routes.reject! do |r|
|
|
102
|
-
r["hostname"] == hostname && (owner_pid.nil? || r["pid"] == owner_pid)
|
|
103
|
-
end
|
|
104
|
-
save_routes(routes)
|
|
105
|
-
end
|
|
110
|
+
# Live routes owned by one agent (for quotas, targeted prune/stop).
|
|
111
|
+
def routes_for(agent)
|
|
112
|
+
load_routes.select { |r| r["agent"] == agent }
|
|
106
113
|
end
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
# Remove stale entries belonging to one agent only; other agents'
|
|
116
|
+
# routes are untouched even when their owners are dead.
|
|
117
|
+
def prune_stale(agent: nil)
|
|
109
118
|
stale = []
|
|
110
119
|
with_lock do
|
|
111
120
|
all = load_routes_raw
|
|
112
121
|
alive, dead = all.partition { |r| r["pid"] == 0 || alive?(r["pid"]) }
|
|
113
|
-
stale = dead
|
|
114
|
-
save_routes(alive) unless
|
|
122
|
+
stale = agent ? dead.select { |r| r["agent"] == agent } : dead
|
|
123
|
+
save_routes(alive + (dead - stale)) unless stale.empty?
|
|
115
124
|
end
|
|
116
125
|
stale
|
|
117
126
|
end
|
|
118
127
|
|
|
128
|
+
def remove_route(hostname, owner_pid: nil)
|
|
129
|
+
with_lock do
|
|
130
|
+
routes = load_routes(prune: true)
|
|
131
|
+
routes.reject! do |r|
|
|
132
|
+
r["hostname"] == hostname && (owner_pid.nil? || r["pid"] == owner_pid)
|
|
133
|
+
end
|
|
134
|
+
save_routes(routes)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
119
138
|
def find(hostname)
|
|
120
139
|
load_routes.find { |r| r["hostname"] == hostname }
|
|
121
140
|
end
|
|
122
141
|
|
|
123
142
|
private
|
|
124
143
|
|
|
144
|
+
# Per-agent route cap. Counts the agent's live routes (excluding the
|
|
145
|
+
# hostname being (re)registered, so restarts never trip the quota).
|
|
146
|
+
# Limit 0 disables the cap. Raises QuotaExceededError naming the
|
|
147
|
+
# agent, the count, and the fix.
|
|
148
|
+
def enforce_quota!(routes, agent, hostname)
|
|
149
|
+
limit = Agent.max_routes
|
|
150
|
+
return if limit.zero?
|
|
151
|
+
|
|
152
|
+
count = routes.count { |r| r["agent"] == agent && r["hostname"] != hostname }
|
|
153
|
+
raise QuotaExceededError.new(agent, limit, count) if count >= limit
|
|
154
|
+
end
|
|
155
|
+
|
|
125
156
|
def valid_route?(value)
|
|
126
157
|
value.is_a?(Hash) &&
|
|
127
158
|
value["hostname"].is_a?(String) &&
|
data/lib/yamine/runner.rb
CHANGED
|
@@ -90,15 +90,18 @@ module Yamine
|
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
# Run an arbitrary command with PORT + YAMINE_URL. Returns App.
|
|
93
|
+
# register: false spawns without a route (background processes).
|
|
93
94
|
def boot_run(name:, hostname:, url:, dir:, command:, port: nil, force: false,
|
|
94
|
-
rails_dev_host: nil)
|
|
95
|
+
rails_dev_host: nil, register: true)
|
|
95
96
|
port ||= Ports.find_free
|
|
96
97
|
env = child_env(dir, url: url, port: port, rails_dev_host: rails_dev_host)
|
|
97
98
|
pid = with_clean_env { spawn(env, *command, chdir: dir) }
|
|
98
99
|
Process.detach(pid)
|
|
99
100
|
target = "127.0.0.1:#{port}"
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
if register
|
|
102
|
+
@store.add_route(hostname, target, Process.pid, kind: "tcp", force: force)
|
|
103
|
+
write_backend_pid(hostname, pid)
|
|
104
|
+
end
|
|
102
105
|
App.new(name: name, hostname: hostname, url: url, pid: pid,
|
|
103
106
|
target: target, kind: "tcp", command: command)
|
|
104
107
|
end
|
data/lib/yamine/version.rb
CHANGED
data/lib/yamine.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yamine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -82,6 +82,7 @@ files:
|
|
|
82
82
|
- bin/yamine
|
|
83
83
|
- lib/ask/skills/yamine/SKILL.md
|
|
84
84
|
- lib/yamine.rb
|
|
85
|
+
- lib/yamine/agent.rb
|
|
85
86
|
- lib/yamine/certs.rb
|
|
86
87
|
- lib/yamine/cli.rb
|
|
87
88
|
- lib/yamine/cli/boot.rb
|