Skip to content

Commit 07cc613

Browse files
committed
feat: Introduce Raftify and RaftGlobalTimer to replace DistributedGlobalTimer
q#
1 parent 5307363 commit 07cc613

24 files changed

Lines changed: 951 additions & 236 deletions

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,6 @@ docs/manager/rest-reference/openapi.json
142142

143143
/DIST-INFO
144144
/INSTALL-INFO
145+
146+
# Raft cluster config
147+
raft-cluster-config.toml

changes/2105.feature.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add Raft-based leader election process to manager group in HA condition in order to make their states consistent.

configs/manager/halfstack.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ pool-pre-ping = false
1616

1717

1818
[manager]
19-
num-proc = 4
19+
num-proc = 3
2020
service-addr = { host = "0.0.0.0", port = 8081 }
2121
#user = "nobody"
2222
#group = "nobody"
@@ -35,6 +35,11 @@ hide-agents = true
3535
# The order of agent selection.
3636
agent-selection-resource-priority = ["cuda", "rocm", "tpu", "cpu", "mem"]
3737

38+
[raft]
39+
heartbeat-tick = 3
40+
election-tick = 10
41+
log-dir = "./logs"
42+
3843
[docker-registry]
3944
ssl-verify = false
4045

@@ -48,6 +53,7 @@ drivers = ["console"]
4853
"aiotools" = "INFO"
4954
"aiohttp" = "INFO"
5055
"ai.backend" = "INFO"
56+
"ai.backend.manager.server.raft" = "INFO"
5157
"alembic" = "INFO"
5258
"sqlalchemy" = "WARNING"
5359

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#restore-wal-from = 1
2+
#restore-wal-snapshot-from = 1
3+
4+
[[peers.myself]]
5+
host = "192.168.0.37"
6+
port = 60151
7+
node-id = 1
8+
role = "voter"
9+
10+
[[peers.myself]]
11+
host = "192.168.0.37"
12+
port = 60152
13+
node-id = 2
14+
role = "voter"
15+
16+
[[peers.myself]]
17+
host = "192.168.0.37"
18+
port = 60153
19+
node-id = 3
20+
role = "voter"

python.lock

Lines changed: 62 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
// "python-dotenv~=0.20.0",
7575
// "python-json-logger>=2.0.1",
7676
// "pyzmq~=25.1.2",
77+
// "raftify==0.1.65",
7778
// "redis[hiredis]==4.5.5",
7879
// "rich~=13.6",
7980
// "setproctitle~=1.3.2",
@@ -121,19 +122,19 @@
121122
"artifacts": [
122123
{
123124
"algorithm": "sha256",
124-
"hash": "ee799435f77e8c3a2a7207c465feae2343a2aa537c38e8f56b629c8a321a02d0",
125-
"url": "https://files.pythonhosted.org/packages/f7/39/b392dc1a8bb58342deacc1ed2b00edf88fd357e6fdf76cc6c8046825f84f/aioconsole-0.7.0-py3-none-any.whl"
125+
"hash": "1867a7cc86897a87398e6e6fba302738548f1cf76cbc6c76e06338e091113bdc",
126+
"url": "https://files.pythonhosted.org/packages/08/17/98008117ec3f484259f11a8a96cb5601949546a4de43102b99cffa1138e5/aioconsole-0.7.1-py3-none-any.whl"
126127
},
127128
{
128129
"algorithm": "sha256",
129-
"hash": "c702d24406378d37d9873f91e03ce71520bac503d5ab03f81d8b563ff010bd54",
130-
"url": "https://files.pythonhosted.org/packages/85/da/6a238a72274fa338b2ff20007f026944a6721245fa65d3bd4adeb83be419/aioconsole-0.7.0.tar.gz"
130+
"hash": "a3e52428d32623c96746ec3862d97483c61c12a2f2dfba618886b709415d4533",
131+
"url": "https://files.pythonhosted.org/packages/e5/f4/f156826819b4136b3fe9fac1b7707f6f241c871aaef13b4a16932e39156d/aioconsole-0.7.1.tar.gz"
131132
}
132133
],
133134
"project_name": "aioconsole",
134135
"requires_dists": [],
135136
"requires_python": ">=3.8",
136-
"version": "0.7.0"
137+
"version": "0.7.1"
137138
},
138139
{
139140
"artifacts": [
@@ -947,36 +948,36 @@
947948
"artifacts": [
948949
{
949950
"algorithm": "sha256",
950-
"hash": "db7bbb1c6059e99b74dcf634e497b04addcac4c527ae2b2696e47c39eccc6c50",
951-
"url": "https://files.pythonhosted.org/packages/4b/bb/f3a77166e1917b4269f13752edbabbd8aa022a442f51e4779247fb9a1253/boto3-1.34.92-py3-none-any.whl"
951+
"hash": "6c8125310005255ea998bccc3e8353b4df81a96ab105c89c118461f6c54c07c8",
952+
"url": "https://files.pythonhosted.org/packages/09/18/6b9e0bbdc28a11c1f953160934cd10c938811345d80c3d9c5719c18fe522/boto3-1.34.97-py3-none-any.whl"
952953
},
953954
{
954955
"algorithm": "sha256",
955-
"hash": "684cba753d64978a486e8ea9645d53de0d4e3b4a3ab1495b26bd04b9541cea2d",
956-
"url": "https://files.pythonhosted.org/packages/c5/d2/57a90069ab4de1780a16d1a670c14098a417b676ab14724402a32feb695a/boto3-1.34.92.tar.gz"
956+
"hash": "60e5dda0b29805fb410bfda1d98e898edaebedac0e6983e9c57cb88e44dfa64e",
957+
"url": "https://files.pythonhosted.org/packages/e5/27/9073116821d6cf73d6463424e3f2d3ab0edaf2ee182c9eb1b263defa3eaf/boto3-1.34.97.tar.gz"
957958
}
958959
],
959960
"project_name": "boto3",
960961
"requires_dists": [
961-
"botocore<1.35.0,>=1.34.92",
962+
"botocore<1.35.0,>=1.34.97",
962963
"botocore[crt]<2.0a0,>=1.21.0; extra == \"crt\"",
963964
"jmespath<2.0.0,>=0.7.1",
964965
"s3transfer<0.11.0,>=0.10.0"
965966
],
966967
"requires_python": ">=3.8",
967-
"version": "1.34.92"
968+
"version": "1.34.97"
968969
},
969970
{
970971
"artifacts": [
971972
{
972973
"algorithm": "sha256",
973-
"hash": "4211a22a1f6c6935e70cbb84c2cd93b29f9723eaf5036d59748dd104f389a681",
974-
"url": "https://files.pythonhosted.org/packages/c4/58/c25d117142140b65f0782bc066b4b52a84454075e4e575bf37d1c7fb0ad9/botocore-1.34.92-py3-none-any.whl"
974+
"hash": "c98b1272e377c69e167cc68c0f2c9c79bc7a6098775eecdad41ee5a28de69324",
975+
"url": "https://files.pythonhosted.org/packages/79/2a/cf6ad65939b48a4a3a984174928abab0955e4627a79bf552c876dd17172b/botocore-1.34.97-py3-none-any.whl"
975976
},
976977
{
977978
"algorithm": "sha256",
978-
"hash": "d1ca4886271f184445ec737cd2e752498648cca383887c5a37b2e01c8ab94039",
979-
"url": "https://files.pythonhosted.org/packages/d5/d0/e263194220495a7a61cad619100db16128bd0a3ab1ea73f1e540905ca29a/botocore-1.34.92.tar.gz"
979+
"hash": "e421b592add68547ed141643c8a8b4aa819a07059b85efd72e89b6758c956420",
980+
"url": "https://files.pythonhosted.org/packages/8e/4d/724915591564af6c31bf6a0a41981542634e3b8000a0a1aa61541719c5b1/botocore-1.34.97.tar.gz"
980981
}
981982
],
982983
"project_name": "botocore",
@@ -988,7 +989,7 @@
988989
"urllib3<1.27,>=1.25.4; python_version < \"3.10\""
989990
],
990991
"requires_python": ">=3.8",
991-
"version": "1.34.92"
992+
"version": "1.34.97"
992993
},
993994
{
994995
"artifacts": [
@@ -2475,13 +2476,13 @@
24752476
"artifacts": [
24762477
{
24772478
"algorithm": "sha256",
2478-
"hash": "f085493f79efb0644f270a9bf2892843142d80d7174bbbd2f3713f2a589dc633",
2479-
"url": "https://files.pythonhosted.org/packages/38/04/37055b7013dfaaf66e3a9a51e46857cc9be151476a891b995fa70da7e139/marshmallow-3.21.1-py3-none-any.whl"
2479+
"hash": "70b54a6282f4704d12c0a41599682c5c5450e843b9ec406308653b47c59648a1",
2480+
"url": "https://files.pythonhosted.org/packages/be/24/cbb242420021a79c87768dcd22ce028f48ef40913239ad6106c8a557f52c/marshmallow-3.21.2-py3-none-any.whl"
24802481
},
24812482
{
24822483
"algorithm": "sha256",
2483-
"hash": "4e65e9e0d80fc9e609574b9983cf32579f305c718afb30d7233ab818571768c3",
2484-
"url": "https://files.pythonhosted.org/packages/5b/17/1b117d1875d8287a85cc2d5e2effd3f31bd8afd9f142c7b8391b9d665f0c/marshmallow-3.21.1.tar.gz"
2484+
"hash": "82408deadd8b33d56338d2182d455db632c6313aa2af61916672146bb32edc56",
2485+
"url": "https://files.pythonhosted.org/packages/a2/16/06ad266adc423f9d7ee49dce26787b973907aa70213760c9fe1711745405/marshmallow-3.21.2.tar.gz"
24852486
}
24862487
],
24872488
"project_name": "marshmallow",
@@ -2494,13 +2495,13 @@
24942495
"pytest; extra == \"tests\"",
24952496
"pytz; extra == \"tests\"",
24962497
"simplejson; extra == \"tests\"",
2497-
"sphinx-issues==4.0.0; extra == \"docs\"",
2498+
"sphinx-issues==4.1.0; extra == \"docs\"",
24982499
"sphinx-version-warning==1.1.2; extra == \"docs\"",
2499-
"sphinx==7.2.6; extra == \"docs\"",
2500+
"sphinx==7.3.7; extra == \"docs\"",
25002501
"tox; extra == \"dev\""
25012502
],
25022503
"requires_python": ">=3.8",
2503-
"version": "3.21.1"
2504+
"version": "3.21.2"
25042505
},
25052506
{
25062507
"artifacts": [
@@ -3316,34 +3317,34 @@
33163317
"artifacts": [
33173318
{
33183319
"algorithm": "sha256",
3319-
"hash": "2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7",
3320-
"url": "https://files.pythonhosted.org/packages/4d/7e/c79cecfdb6aa85c6c2e3cf63afc56d0f165f24f5c66c03c695c4d9b84756/pytest-8.1.1-py3-none-any.whl"
3320+
"hash": "1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233",
3321+
"url": "https://files.pythonhosted.org/packages/c4/43/6b1debd95ecdf001bc46789a933f658da3f9738c65f32db3f4e8f2a4ca97/pytest-8.2.0-py3-none-any.whl"
33213322
},
33223323
{
33233324
"algorithm": "sha256",
3324-
"hash": "ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044",
3325-
"url": "https://files.pythonhosted.org/packages/30/b7/7d44bbc04c531dcc753056920e0988032e5871ac674b5a84cb979de6e7af/pytest-8.1.1.tar.gz"
3325+
"hash": "d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f",
3326+
"url": "https://files.pythonhosted.org/packages/09/9d/78b3785134306efe9329f40815af45b9215068d6ae4747ec0bc91ff1f4aa/pytest-8.2.0.tar.gz"
33263327
}
33273328
],
33283329
"project_name": "pytest",
33293330
"requires_dists": [
3330-
"argcomplete; extra == \"testing\"",
3331-
"attrs>=19.2; extra == \"testing\"",
3331+
"argcomplete; extra == \"dev\"",
3332+
"attrs>=19.2; extra == \"dev\"",
33323333
"colorama; sys_platform == \"win32\"",
33333334
"exceptiongroup>=1.0.0rc8; python_version < \"3.11\"",
3334-
"hypothesis>=3.56; extra == \"testing\"",
3335+
"hypothesis>=3.56; extra == \"dev\"",
33353336
"iniconfig",
3336-
"mock; extra == \"testing\"",
3337+
"mock; extra == \"dev\"",
33373338
"packaging",
3338-
"pluggy<2.0,>=1.4",
3339-
"pygments>=2.7.2; extra == \"testing\"",
3340-
"requests; extra == \"testing\"",
3341-
"setuptools; extra == \"testing\"",
3339+
"pluggy<2.0,>=1.5",
3340+
"pygments>=2.7.2; extra == \"dev\"",
3341+
"requests; extra == \"dev\"",
3342+
"setuptools; extra == \"dev\"",
33423343
"tomli>=1; python_version < \"3.11\"",
3343-
"xmlschema; extra == \"testing\""
3344+
"xmlschema; extra == \"dev\""
33443345
],
33453346
"requires_python": ">=3.8",
3346-
"version": "8.1.1"
3347+
"version": "8.2.0"
33473348
},
33483349
{
33493350
"artifacts": [
@@ -3535,6 +3536,24 @@
35353536
"requires_python": ">=3.6",
35363537
"version": "25.1.2"
35373538
},
3539+
{
3540+
"artifacts": [
3541+
{
3542+
"algorithm": "sha256",
3543+
"hash": "04c5983aed2c700eb0f3100ac09e51ce5c65ba39d8a627ec138d385261a6a850",
3544+
"url": "https://files.pythonhosted.org/packages/78/76/118c5e454a68014e4580cdc2aa0cb87a12c51f40bc57b420ae3daec102ee/raftify-0.1.65-cp312-cp312-macosx_11_0_arm64.whl"
3545+
},
3546+
{
3547+
"algorithm": "sha256",
3548+
"hash": "71abf9d3e3e829850cba5b013a42c6e9ef2d556585bf224de5aff75a182a93c2",
3549+
"url": "https://files.pythonhosted.org/packages/df/e0/49cb08a23f49a232d94fd3a32f534654750930e98cf85d074a9b4be3582b/raftify-0.1.65.tar.gz"
3550+
}
3551+
],
3552+
"project_name": "raftify",
3553+
"requires_dists": [],
3554+
"requires_python": ">=3.10",
3555+
"version": "0.1.65"
3556+
},
35383557
{
35393558
"artifacts": [
35403559
{
@@ -4090,13 +4109,13 @@
40904109
"artifacts": [
40914110
{
40924111
"algorithm": "sha256",
4093-
"hash": "1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9",
4094-
"url": "https://files.pythonhosted.org/packages/2a/14/e75e52d521442e2fcc9f1df3c5e456aead034203d4797867980de558ab34/tqdm-4.66.2-py3-none-any.whl"
4112+
"hash": "b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644",
4113+
"url": "https://files.pythonhosted.org/packages/18/eb/fdb7eb9e48b7b02554e1664afd3bd3f117f6b6d6c5881438a0b055554f9b/tqdm-4.66.4-py3-none-any.whl"
40954114
},
40964115
{
40974116
"algorithm": "sha256",
4098-
"hash": "6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531",
4099-
"url": "https://files.pythonhosted.org/packages/ea/85/3ce0f9f7d3f596e7ea57f4e5ce8c18cb44e4a9daa58ddb46ee0d13d6bff8/tqdm-4.66.2.tar.gz"
4117+
"hash": "e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb",
4118+
"url": "https://files.pythonhosted.org/packages/5a/c0/b7599d6e13fe0844b0cda01b9aaef9a0e87dbb10b06e4ee255d3fa1c79a2/tqdm-4.66.4.tar.gz"
41004119
}
41014120
],
41024121
"project_name": "tqdm",
@@ -4111,7 +4130,7 @@
41114130
"slack-sdk; extra == \"slack\""
41124131
],
41134132
"requires_python": ">=3.7",
4114-
"version": "4.66.2"
4133+
"version": "4.66.4"
41154134
},
41164135
{
41174136
"artifacts": [
@@ -4799,6 +4818,7 @@
47994818
"python-dotenv~=0.20.0",
48004819
"python-json-logger>=2.0.1",
48014820
"pyzmq~=25.1.2",
4821+
"raftify==0.1.65",
48024822
"redis[hiredis]==4.5.5",
48034823
"rich~=13.6",
48044824
"setproctitle~=1.3.2",

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,4 @@ backend.ai-krunner-alpine==5.2.0
9595
backend.ai-krunner-static-gnu==4.2.0
9696

9797
etcd-client-py==0.3.0
98+
raftify==0.1.65

src/ai/backend/common/distributed.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from __future__ import annotations
22

3+
import abc
34
import asyncio
45
import logging
56
from typing import TYPE_CHECKING, Callable, Final
67

78
from aiomonitor.task import preserve_termination_log
9+
from raftify import RaftNode
810

911
from .logging import BraceStyleAdapter
1012

@@ -16,7 +18,77 @@
1618
log = BraceStyleAdapter(logging.getLogger(__spec__.name)) # type: ignore[name-defined]
1719

1820

19-
class GlobalTimer:
21+
class AbstractGlobalTimer(metaclass=abc.ABCMeta):
22+
@abc.abstractmethod
23+
async def generate_tick(self) -> None:
24+
raise NotImplementedError
25+
26+
@abc.abstractmethod
27+
async def join(self) -> None:
28+
raise NotImplementedError
29+
30+
@abc.abstractmethod
31+
async def leave(self) -> None:
32+
raise NotImplementedError
33+
34+
35+
class RaftGlobalTimer(AbstractGlobalTimer):
36+
"""
37+
Executes the given async function only once in the given interval,
38+
uniquely among multiple manager instances across multiple nodes.
39+
"""
40+
41+
_event_producer: Final[EventProducer]
42+
43+
def __init__(
44+
self,
45+
raft_node: RaftNode,
46+
event_producer: EventProducer,
47+
event_factory: Callable[[], AbstractEvent],
48+
interval: float = 10.0,
49+
initial_delay: float = 0.0,
50+
) -> None:
51+
self._event_producer = event_producer
52+
self._event_factory = event_factory
53+
self._stopped = False
54+
self.interval = interval
55+
self.initial_delay = initial_delay
56+
self.raft_node = raft_node
57+
58+
async def generate_tick(self) -> None:
59+
try:
60+
await asyncio.sleep(self.initial_delay)
61+
if self._stopped:
62+
return
63+
while True:
64+
try:
65+
if self._stopped:
66+
return
67+
if await self.raft_node.is_leader():
68+
await self._event_producer.produce_event(self._event_factory())
69+
if self._stopped:
70+
return
71+
await asyncio.sleep(self.interval)
72+
except asyncio.TimeoutError: # timeout raised from etcd lock
73+
log.warn("timeout raised while trying to acquire lock. retrying...")
74+
except asyncio.CancelledError:
75+
pass
76+
77+
async def join(self) -> None:
78+
self._tick_task = asyncio.create_task(self.generate_tick())
79+
80+
async def leave(self) -> None:
81+
self._stopped = True
82+
await asyncio.sleep(0)
83+
if not self._tick_task.done():
84+
try:
85+
self._tick_task.cancel()
86+
await self._tick_task
87+
except asyncio.CancelledError:
88+
pass
89+
90+
91+
class DistributedLockGlobalTimer(AbstractGlobalTimer):
2092
"""
2193
Executes the given async function only once in the given interval,
2294
uniquely among multiple manager instances across multiple nodes.

0 commit comments

Comments
 (0)