Skip to content

Commit 803bdc7

Browse files
committed
Add deployment
1 parent 9e3c983 commit 803bdc7

19 files changed

Lines changed: 1463 additions & 174 deletions

File tree

k8s/app-chart/templates/codebattle_deployment.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ spec:
1717
labels:
1818
app: "codebattle"
1919
spec:
20+
terminationGracePeriodSeconds: {{ .Values.codebattle.rollout.terminationGracePeriodSeconds }}
2021
{{- with .Values.codebattle.affinity }}
2122
affinity:
2223
{{- toYaml . | nindent 8 }}
@@ -48,12 +49,27 @@ spec:
4849
command:
4950
- make
5051
- start
52+
lifecycle:
53+
preStop:
54+
exec:
55+
command:
56+
- /bin/sh
57+
- -c
58+
- "curl -s -X POST http://127.0.0.1:4000/health/drain || true; curl -s -X POST http://127.0.0.1:4000/health/handoff || true; until [ \"$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:4000/health/release_ready)\" = \"200\" ]; do sleep 2; done; sleep {{ .Values.codebattle.rollout.preStopSleepSeconds }}"
5159
envFrom:
5260
- secretRef:
5361
name: codebattle-secrets
5462
- configMapRef:
5563
name: codebattle-env
5664
optional: true
5765
env:
66+
- name: POD_IP
67+
valueFrom:
68+
fieldRef:
69+
fieldPath: status.podIP
70+
- name: POD_NAME
71+
valueFrom:
72+
fieldRef:
73+
fieldPath: metadata.name
5874
- name: DEPLOYED_AT
5975
value: {{ dateInZone "2006-01-02 15:04:05" (now) "UTC" }}

k8s/app-chart/values.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ codebattle:
1818
image:
1919
repository: hexlet-codebattle/codebattle
2020
imagePullPolicy: Always
21+
rollout:
22+
preStopSleepSeconds: 5
23+
terminationGracePeriodSeconds: 7200
2124
service:
2225
type: ClusterIP
2326
port: 4000

services/app/apps/codebattle/assets/js/socket.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ export const channelTopics = {
5656
tournamentPlayerFinishedTopic: 'tournament:player:finished',
5757
tournamentActivated: 'tournament:activated',
5858
tournamentCanceled: 'tournament:canceled',
59+
deployHandoffStarted: 'deploy:handoff_started',
60+
deployHandoffDone: 'deploy:handoff_done',
61+
deployHandoffFailed: 'deploy:handoff_failed',
5962

6063
roundCreatedTopic: 'round:created',
6164

services/app/apps/codebattle/assets/js/widgets/middlewares/Main.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,38 @@ const redirectToNewGame = (data) => (_dispatch, getState) => {
7171
}
7272
};
7373

74+
const deployBannerId = 'cb-deploy-handoff-banner';
75+
76+
const renderDeployBanner = (text, backgroundColor = '#2f3747') => {
77+
const existing = document.getElementById(deployBannerId);
78+
const el = existing || document.createElement('div');
79+
80+
el.id = deployBannerId;
81+
el.textContent = text;
82+
el.style.position = 'fixed';
83+
el.style.top = '0';
84+
el.style.left = '0';
85+
el.style.right = '0';
86+
el.style.zIndex = '2000';
87+
el.style.padding = '8px 12px';
88+
el.style.textAlign = 'center';
89+
el.style.fontSize = '14px';
90+
el.style.color = '#ffffff';
91+
el.style.backgroundColor = backgroundColor;
92+
93+
if (!existing) {
94+
document.body.appendChild(el);
95+
}
96+
};
97+
98+
const removeDeployBanner = () => {
99+
const existing = document.getElementById(deployBannerId);
100+
101+
if (existing) {
102+
existing.remove();
103+
}
104+
};
105+
74106
const initPresence = (followId) => (dispatch) => {
75107
channel = new Channel('main', {
76108
...getUserStateByPath(),
@@ -109,6 +141,25 @@ const initPresence = (followId) => (dispatch) => {
109141
(data) => {
110142
camelizeKeysAndDispatch(dispatch, actions.changeTournamentState)(data);
111143
},
144+
).addListener(
145+
channelTopics.deployHandoffStarted,
146+
() => {
147+
renderDeployBanner('Deploy in progress. Reconnecting game session...');
148+
},
149+
).addListener(
150+
channelTopics.deployHandoffDone,
151+
() => {
152+
renderDeployBanner('Deploy finished. Syncing latest session...', '#3a8b3a');
153+
setTimeout(() => {
154+
removeDeployBanner();
155+
window.location.reload();
156+
}, 1200);
157+
},
158+
).addListener(
159+
channelTopics.deployHandoffFailed,
160+
() => {
161+
renderDeployBanner('Deploy handoff incomplete. Reconnecting...', '#b34d4d');
162+
},
112163
);
113164
};
114165

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
defmodule Codebattle.Cluster do
2+
@moduledoc false
3+
4+
@default_wait_timeout_ms 15_000
5+
@retry_delay_ms 500
6+
7+
def connected_nodes do
8+
Node.list(:connected)
9+
end
10+
11+
def choose_target_node do
12+
env_target = System.get_env("CODEBATTLE_HANDOFF_TARGET_NODE")
13+
14+
case parse_node(env_target) do
15+
nil ->
16+
connected_nodes()
17+
|> Enum.sort()
18+
|> List.first()
19+
20+
target_node ->
21+
if target_node in connected_nodes(), do: target_node
22+
end
23+
end
24+
25+
def wait_for_target_node(timeout_ms \\ @default_wait_timeout_ms) do
26+
if is_nil(parse_node(System.get_env("CODEBATTLE_HANDOFF_TARGET_NODE"))) and connected_nodes() == [] do
27+
nil
28+
else
29+
deadline = System.monotonic_time(:millisecond) + timeout_ms
30+
wait_for_target_node_until(deadline)
31+
end
32+
end
33+
34+
defp wait_for_target_node_until(deadline) do
35+
case choose_target_node() do
36+
nil ->
37+
if System.monotonic_time(:millisecond) < deadline do
38+
Process.sleep(@retry_delay_ms)
39+
wait_for_target_node_until(deadline)
40+
end
41+
42+
node ->
43+
node
44+
end
45+
end
46+
47+
defp parse_node(nil), do: nil
48+
defp parse_node(""), do: nil
49+
50+
defp parse_node(raw) when is_binary(raw) do
51+
raw
52+
|> String.trim()
53+
|> case do
54+
"" -> nil
55+
value -> String.to_atom(value)
56+
end
57+
end
58+
end

0 commit comments

Comments
 (0)