mirror of
https://github.com/neondatabase/neon.git
synced 2026-02-06 20:20:37 +00:00
Compare commits
591 Commits
proxy-cpla
...
compress-p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c78a5067f | ||
|
|
108f08f982 | ||
|
|
5700233a47 | ||
|
|
1d66ca79a9 | ||
|
|
23827c6b0d | ||
|
|
66b0bf41a1 | ||
|
|
89cf8df93b | ||
|
|
54a06de4b5 | ||
|
|
6f20a18e8e | ||
|
|
d557002675 | ||
|
|
32b75e7c73 | ||
|
|
d2753719e3 | ||
|
|
04b2ac3fed | ||
|
|
c39d5b03e8 | ||
|
|
76fc3d4aa1 | ||
|
|
dd3adc3693 | ||
|
|
5b871802fd | ||
|
|
24ce73ffaf | ||
|
|
3118c24521 | ||
|
|
5af9660b9e | ||
|
|
d7e349d33c | ||
|
|
47e5bf3bbb | ||
|
|
5d2f9ffa89 | ||
|
|
fdadd6a152 | ||
|
|
9b623d3a2c | ||
|
|
9b98823d61 | ||
|
|
76864e6a2a | ||
|
|
6c5d3b5263 | ||
|
|
cd9a550d97 | ||
|
|
07f21dd6b6 | ||
|
|
64a4461191 | ||
|
|
961fc0ba8f | ||
|
|
9b2f9419d9 | ||
|
|
947f6da75e | ||
|
|
7026dde9eb | ||
|
|
d502313841 | ||
|
|
219e78f885 | ||
|
|
1ea5d8b132 | ||
|
|
3d760938e1 | ||
|
|
9211de0df7 | ||
|
|
d8ffe662a9 | ||
|
|
a4db2af1f0 | ||
|
|
47fdf93cf0 | ||
|
|
de05f90735 | ||
|
|
188797f048 | ||
|
|
5446e08891 | ||
|
|
78d9059fc7 | ||
|
|
75747cdbff | ||
|
|
8fe3f17c47 | ||
|
|
8776089c70 | ||
|
|
b74232eb4d | ||
|
|
ee3081863e | ||
|
|
15728be0e1 | ||
|
|
f45cf28247 | ||
|
|
82266a252c | ||
|
|
59f949b4a8 | ||
|
|
01399621d5 | ||
|
|
0792bb6785 | ||
|
|
f8ac3b0e0e | ||
|
|
02ecdd137b | ||
|
|
79401638df | ||
|
|
c789ec21f6 | ||
|
|
558a57b15b | ||
|
|
f0e2bb79b2 | ||
|
|
fd0b22f5cd | ||
|
|
56da624870 | ||
|
|
b998b70315 | ||
|
|
76aa6936e8 | ||
|
|
438fd2aaf3 | ||
|
|
e7d62a257d | ||
|
|
5778d714f0 | ||
|
|
4753b8f390 | ||
|
|
68476bb4ba | ||
|
|
6bb8b1d7c2 | ||
|
|
30b890e378 | ||
|
|
560627b525 | ||
|
|
1c1b4b0c04 | ||
|
|
b774ab54d4 | ||
|
|
33a09946fc | ||
|
|
0396ed67f7 | ||
|
|
8ee6724167 | ||
|
|
8a9fa0a4e4 | ||
|
|
cf60e4c0c5 | ||
|
|
68a2298973 | ||
|
|
4feb6ba29c | ||
|
|
29a41fc7b9 | ||
|
|
d8b2a49c55 | ||
|
|
ed9ffb9af2 | ||
|
|
6c6a7f9ace | ||
|
|
e729f28205 | ||
|
|
b6e1c09c73 | ||
|
|
16d80128ee | ||
|
|
2ba414525e | ||
|
|
46210035c5 | ||
|
|
81892199f6 | ||
|
|
83eb02b07a | ||
|
|
a71f58e69c | ||
|
|
e6eb0020a1 | ||
|
|
eb0ca9b648 | ||
|
|
6843fd8f89 | ||
|
|
edc900028e | ||
|
|
789196572e | ||
|
|
425eed24e8 | ||
|
|
f67010109f | ||
|
|
0c3e3a8667 | ||
|
|
82719542c6 | ||
|
|
d25f7e3dd5 | ||
|
|
fbccd1e676 | ||
|
|
dc2ab4407f | ||
|
|
ad0ab3b81b | ||
|
|
836d1f4af7 | ||
|
|
9dda13ecce | ||
|
|
9ba9f32dfe | ||
|
|
3099e1a787 | ||
|
|
f749437cec | ||
|
|
0a256148b0 | ||
|
|
69aa1aca35 | ||
|
|
9983ae291b | ||
|
|
b7a0c2b614 | ||
|
|
27518676d7 | ||
|
|
78a59b94f5 | ||
|
|
7121db3669 | ||
|
|
126bcc3794 | ||
|
|
4c2100794b | ||
|
|
d3b892e9ad | ||
|
|
7515d0f368 | ||
|
|
e27ce38619 | ||
|
|
e46692788e | ||
|
|
a8ca7a1a1d | ||
|
|
b52e31c1a4 | ||
|
|
5a7e285c2c | ||
|
|
ae5badd375 | ||
|
|
3e63d0f9e0 | ||
|
|
3b647cd55d | ||
|
|
26c68f91f3 | ||
|
|
2078dc827b | ||
|
|
8ee191c271 | ||
|
|
66c6b270f1 | ||
|
|
e4e444f59f | ||
|
|
d46d19456d | ||
|
|
5d05013857 | ||
|
|
014509987d | ||
|
|
75bca9bb19 | ||
|
|
a8be07785e | ||
|
|
630cfbe420 | ||
|
|
0a65333fff | ||
|
|
91dd99038e | ||
|
|
83ab14e271 | ||
|
|
85ef6b1645 | ||
|
|
1a8d53ab9d | ||
|
|
3d6e389aa2 | ||
|
|
17116f2ea9 | ||
|
|
fd22fc5b7d | ||
|
|
0112097e13 | ||
|
|
9d4c113f9b | ||
|
|
0acb604fa3 | ||
|
|
387a36874c | ||
|
|
00032c9d9f | ||
|
|
11bb265de1 | ||
|
|
69026a9a36 | ||
|
|
7006caf3a1 | ||
|
|
69d18d6429 | ||
|
|
acf0a11fea | ||
|
|
c1f55c1525 | ||
|
|
34f450c05a | ||
|
|
db477c0b8c | ||
|
|
a345cf3fc6 | ||
|
|
e98bc4fd2b | ||
|
|
7e60563910 | ||
|
|
ef83f31e77 | ||
|
|
9fda85b486 | ||
|
|
87afbf6b24 | ||
|
|
16b2e74037 | ||
|
|
5a394fde56 | ||
|
|
7ec70b5eff | ||
|
|
1fcc2b37eb | ||
|
|
af40bf3c2e | ||
|
|
e6db8069b0 | ||
|
|
98dadf8543 | ||
|
|
c18b1c0646 | ||
|
|
f20a9e760f | ||
|
|
33395dcf4e | ||
|
|
1eca8b8a6b | ||
|
|
167394a073 | ||
|
|
9a081c230f | ||
|
|
fddd11dd1a | ||
|
|
238fa47bee | ||
|
|
b0a954bde2 | ||
|
|
7ac11d3942 | ||
|
|
c8cebecabf | ||
|
|
14df69d0e3 | ||
|
|
352b08d0be | ||
|
|
f9f69a2ee7 | ||
|
|
fabeff822f | ||
|
|
4a0ce9512b | ||
|
|
d61e924103 | ||
|
|
b2d34a82b9 | ||
|
|
3797566c36 | ||
|
|
43f9a16e46 | ||
|
|
71a7fd983e | ||
|
|
a3f5b83677 | ||
|
|
1455f5a261 | ||
|
|
3860bc9c6c | ||
|
|
c1f4028fc0 | ||
|
|
0e4f182680 | ||
|
|
ea2e830707 | ||
|
|
7cf726e36e | ||
|
|
6b3164269c | ||
|
|
75a52ac7fd | ||
|
|
e28e46f20b | ||
|
|
d5d15eb6eb | ||
|
|
49d7f9b5a4 | ||
|
|
95a49f0075 | ||
|
|
545f7e8cd7 | ||
|
|
cd6d811213 | ||
|
|
8f3c316bae | ||
|
|
58e31fe098 | ||
|
|
a43a1ad1df | ||
|
|
eb0c026aac | ||
|
|
ff560a1113 | ||
|
|
4a278cce7c | ||
|
|
f98fdd20e3 | ||
|
|
014f822a78 | ||
|
|
ddd8ebd253 | ||
|
|
9cfe08e3d9 | ||
|
|
64577cfddc | ||
|
|
37f81289c2 | ||
|
|
9217564026 | ||
|
|
3404e76a51 | ||
|
|
62aac6c8ad | ||
|
|
e015b2bf3e | ||
|
|
a7f31f1a59 | ||
|
|
325f3784f9 | ||
|
|
900f391115 | ||
|
|
8901ce9c99 | ||
|
|
ce44dfe353 | ||
|
|
d1d55bbd9f | ||
|
|
df9ab1b5e3 | ||
|
|
ef96c82c9f | ||
|
|
b43f6daa48 | ||
|
|
664f92dc6e | ||
|
|
bd5cb9e86b | ||
|
|
00d66e8012 | ||
|
|
679e031cf6 | ||
|
|
e3f6a07ca3 | ||
|
|
a8a88ba7bc | ||
|
|
353afe4fe7 | ||
|
|
1988ad8db7 | ||
|
|
e3415706b7 | ||
|
|
9d081851ec | ||
|
|
781352bd8e | ||
|
|
8030b8e4c5 | ||
|
|
9a4b896636 | ||
|
|
e8b8ebfa1d | ||
|
|
d9d471e3c4 | ||
|
|
d43dcceef9 | ||
|
|
f2771a99b7 | ||
|
|
f54c3b96e0 | ||
|
|
478cc37a70 | ||
|
|
4ce6e2d2fc | ||
|
|
baeb58432f | ||
|
|
6f3e043a76 | ||
|
|
6810d2aa53 | ||
|
|
2d7091871f | ||
|
|
7701ca45dd | ||
|
|
de8dfee4bd | ||
|
|
e3f51abadf | ||
|
|
a7b84cca5a | ||
|
|
291fcb9e4f | ||
|
|
a5ecca976e | ||
|
|
5caee4ca54 | ||
|
|
e1a9669d05 | ||
|
|
aaf60819fa | ||
|
|
c84656a53e | ||
|
|
af99c959ef | ||
|
|
a8e6d259cb | ||
|
|
c1390bfc3b | ||
|
|
6d951e69d6 | ||
|
|
4b8809b280 | ||
|
|
4c5afb7b10 | ||
|
|
ec069dc45e | ||
|
|
790c05d675 | ||
|
|
923cf91aa4 | ||
|
|
03c6039707 | ||
|
|
c6d5ff944d | ||
|
|
4b97683338 | ||
|
|
affc18f912 | ||
|
|
3ef6e21211 | ||
|
|
1075386d77 | ||
|
|
c3dd646ab3 | ||
|
|
bc78b0e9cc | ||
|
|
f342b87f30 | ||
|
|
438bacc32e | ||
|
|
1a2a3cb446 | ||
|
|
4eedb3b6f1 | ||
|
|
e67fcf9563 | ||
|
|
82960b2175 | ||
|
|
30d15ad403 | ||
|
|
b6ee91835b | ||
|
|
df0f1e359b | ||
|
|
cd0e344938 | ||
|
|
22afaea6e1 | ||
|
|
ba20752b76 | ||
|
|
3a6fa76828 | ||
|
|
9ffb852359 | ||
|
|
972470b174 | ||
|
|
1412e9b3e8 | ||
|
|
be0c73f8e7 | ||
|
|
7f51764001 | ||
|
|
4d8a10af1c | ||
|
|
55ba885f6b | ||
|
|
6ff74295b5 | ||
|
|
bbe730d7ca | ||
|
|
5a0da93c53 | ||
|
|
d9dcbffac3 | ||
|
|
f50ff14560 | ||
|
|
b58a615197 | ||
|
|
1a1d527875 | ||
|
|
216fc5ba7b | ||
|
|
4270e86eb2 | ||
|
|
6351313ae9 | ||
|
|
95098c3216 | ||
|
|
d7c68dc981 | ||
|
|
6206f76419 | ||
|
|
d7f34bc339 | ||
|
|
86905c1322 | ||
|
|
0b02043ba4 | ||
|
|
873b222080 | ||
|
|
13d9589c35 | ||
|
|
be1a88e574 | ||
|
|
b9fd8dcf13 | ||
|
|
5ea117cddf | ||
|
|
2682e0254f | ||
|
|
41fb838799 | ||
|
|
107f535294 | ||
|
|
39c712f2ca | ||
|
|
ab10523cc1 | ||
|
|
d5399b729b | ||
|
|
b06eec41fa | ||
|
|
ca154d9cd8 | ||
|
|
1173ee6a7e | ||
|
|
21e1a496a3 | ||
|
|
0457980728 | ||
|
|
8728d5a5fd | ||
|
|
a4a4d78993 | ||
|
|
870786bd82 | ||
|
|
b6d547cf92 | ||
|
|
e3a2631df9 | ||
|
|
02d42861e4 | ||
|
|
586e77bb24 | ||
|
|
b827e7b330 | ||
|
|
26b1483204 | ||
|
|
d709bcba81 | ||
|
|
b158a5eda0 | ||
|
|
0c99e5ec6d | ||
|
|
0af66a6003 | ||
|
|
017c34b773 | ||
|
|
308227fa51 | ||
|
|
d041f9a887 | ||
|
|
ea531d448e | ||
|
|
2dbd1c1ed5 | ||
|
|
51376ef3c8 | ||
|
|
5a3d8e75ed | ||
|
|
6e4e578841 | ||
|
|
3c9b484c4d | ||
|
|
af849a1f61 | ||
|
|
ac7dc82103 | ||
|
|
f1b654b77d | ||
|
|
7dd58e1449 | ||
|
|
f3af5f4660 | ||
|
|
a96e15cb6b | ||
|
|
df1def7018 | ||
|
|
69337be5c2 | ||
|
|
67a2215163 | ||
|
|
3764dd2e84 | ||
|
|
0115fe6cb2 | ||
|
|
e6da7e29ed | ||
|
|
0353a72a00 | ||
|
|
ce4d3da3ae | ||
|
|
5da3e2113a | ||
|
|
4deb8dc52e | ||
|
|
64f0613edf | ||
|
|
1e7cd6ac9f | ||
|
|
ef03b38e52 | ||
|
|
9b65946566 | ||
|
|
a3fe12b6d8 | ||
|
|
b5a6e68e68 | ||
|
|
ce0ddd749c | ||
|
|
426598cf76 | ||
|
|
8b4dd5dc27 | ||
|
|
ed9a114bde | ||
|
|
b7385bb016 | ||
|
|
37b1930b2f | ||
|
|
d76963691f | ||
|
|
60f570c70d | ||
|
|
3582a95c87 | ||
|
|
00423152c6 | ||
|
|
240efb82f9 | ||
|
|
5f099dc760 | ||
|
|
7a49e5d5c2 | ||
|
|
45ec8688ea | ||
|
|
4b55dad813 | ||
|
|
ab95942fc2 | ||
|
|
f656db09a4 | ||
|
|
69bf1bae7d | ||
|
|
25af32e834 | ||
|
|
cb4b4750ba | ||
|
|
d43d77389e | ||
|
|
5558457c84 | ||
|
|
26e6ff8ba6 | ||
|
|
50a45e67dc | ||
|
|
fcbe60f436 | ||
|
|
e018cac1f7 | ||
|
|
a74b60066c | ||
|
|
3a2f10712a | ||
|
|
4ac4b21598 | ||
|
|
9f792f9c0b | ||
|
|
7434674d86 | ||
|
|
ea37234ccc | ||
|
|
3da54e6d90 | ||
|
|
010f0a310a | ||
|
|
eb53345d48 | ||
|
|
45c625fb34 | ||
|
|
84b6b95783 | ||
|
|
577982b778 | ||
|
|
574645412b | ||
|
|
11945e64ec | ||
|
|
cddafc79e1 | ||
|
|
af7cca4949 | ||
|
|
89cae64e38 | ||
|
|
1f417af9fd | ||
|
|
1684bbf162 | ||
|
|
90cadfa986 | ||
|
|
2226acef7c | ||
|
|
24ce878039 | ||
|
|
84914434e3 | ||
|
|
b655c7030f | ||
|
|
3695a1efa1 | ||
|
|
75b4440d07 | ||
|
|
ee3437cbd8 | ||
|
|
dbe0aa653a | ||
|
|
39427925c2 | ||
|
|
af43f78561 | ||
|
|
ed57772793 | ||
|
|
f1de18f1c9 | ||
|
|
dbb0c967d5 | ||
|
|
bf369f4268 | ||
|
|
70f4a16a05 | ||
|
|
d63185fa6c | ||
|
|
ca8fca0e9f | ||
|
|
0397427dcf | ||
|
|
a2a44ea213 | ||
|
|
4917f52c88 | ||
|
|
04a682021f | ||
|
|
c59abedd85 | ||
|
|
5357f40183 | ||
|
|
e4a279db13 | ||
|
|
b1d47f3911 | ||
|
|
a3d62b31bb | ||
|
|
cdccab4bd9 | ||
|
|
e8814b6f81 | ||
|
|
c18d3340b5 | ||
|
|
447a063f3c | ||
|
|
c12861cccd | ||
|
|
2a3a8ee31d | ||
|
|
5dda371c2b | ||
|
|
a60035b23a | ||
|
|
18fd73d84a | ||
|
|
ee9ec26808 | ||
|
|
e22c072064 | ||
|
|
89f023e6b0 | ||
|
|
8426fb886b | ||
|
|
28e7fa98c4 | ||
|
|
a9fda8c832 | ||
|
|
fa12d60237 | ||
|
|
d551bfee09 | ||
|
|
e69ff3fc00 | ||
|
|
25d9dc6eaf | ||
|
|
139d1346d5 | ||
|
|
0bd16182f7 | ||
|
|
6a5650d40c | ||
|
|
47addc15f1 | ||
|
|
b91c58a8bf | ||
|
|
00d9c2d9a8 | ||
|
|
3a673dce67 | ||
|
|
35e9fb360b | ||
|
|
0d21187322 | ||
|
|
e8a98adcd0 | ||
|
|
98be8b9430 | ||
|
|
6eb946e2de | ||
|
|
681a04d287 | ||
|
|
3df67bf4d7 | ||
|
|
0d8e68003a | ||
|
|
637ad4a638 | ||
|
|
8d0f701767 | ||
|
|
5191f6ef0e | ||
|
|
a54ea8fb1c | ||
|
|
d5708e7435 | ||
|
|
fd49005cb3 | ||
|
|
3023de156e | ||
|
|
e49e931bc4 | ||
|
|
13b9135d4e | ||
|
|
41bb1e42b8 | ||
|
|
cb4b40f9c1 | ||
|
|
9e567d9814 | ||
|
|
1c012958c7 | ||
|
|
e5c50bb12b | ||
|
|
926662eb7c | ||
|
|
3366cd34ba | ||
|
|
2d5a8462c8 | ||
|
|
110282ee7e | ||
|
|
f752c40f58 | ||
|
|
83cdbbb89a | ||
|
|
5288f9621e | ||
|
|
e8338c60f9 | ||
|
|
94505fd672 | ||
|
|
e92fb94149 | ||
|
|
40f15c3123 | ||
|
|
5299f917d6 | ||
|
|
99a56b5606 | ||
|
|
1628b5b145 | ||
|
|
db72543f4d | ||
|
|
d47e4a2a41 | ||
|
|
f86845f64b | ||
|
|
0bb04ebe19 | ||
|
|
5efe95a008 | ||
|
|
c0ff4f18dc | ||
|
|
fd88d4608c | ||
|
|
221414de4b | ||
|
|
dbac2d2c47 | ||
|
|
4f4f787119 | ||
|
|
bcab344490 | ||
|
|
f212630da2 | ||
|
|
a306d0a54b | ||
|
|
1081a4d246 | ||
|
|
47b705cffe | ||
|
|
2d3c9f0d43 | ||
|
|
21b3e1d13b | ||
|
|
0788760451 | ||
|
|
74b2314a5d | ||
|
|
edcaae6290 | ||
|
|
4fc95d2d71 | ||
|
|
534c099b42 | ||
|
|
ec01292b55 | ||
|
|
66fc465484 | ||
|
|
55da8eff4f | ||
|
|
0fa517eb80 | ||
|
|
8ceb4f0a69 | ||
|
|
6019ccef06 | ||
|
|
0c6367a732 | ||
|
|
e17bc6afb4 | ||
|
|
ac7fc6110b | ||
|
|
862a6b7018 | ||
|
|
4810c22607 | ||
|
|
9d754e984f | ||
|
|
375e15815c | ||
|
|
7ce613354e | ||
|
|
ae15acdee7 | ||
|
|
c5f64fe54f | ||
|
|
40852b955d | ||
|
|
b30b15e7cb | ||
|
|
36b875388f | ||
|
|
3f77f26aa2 | ||
|
|
8b10407be4 | ||
|
|
944313ffe1 | ||
|
|
d443d07518 | ||
|
|
3de416a016 | ||
|
|
bc05d7eb9c | ||
|
|
d8da51e78a | ||
|
|
6e3834d506 | ||
|
|
582cec53c5 | ||
|
|
9957c6a9a0 | ||
|
|
a5777bab09 | ||
|
|
90a8ff55fa | ||
|
|
3b95e8072a | ||
|
|
8ee54ffd30 | ||
|
|
3ab9f56f5f | ||
|
|
7ddc7b4990 | ||
|
|
63213fc814 | ||
|
|
090123a429 | ||
|
|
39d1818ae9 | ||
|
|
90be79fcf5 | ||
|
|
c52b80b930 | ||
|
|
722f271f6e | ||
|
|
be1d8fc4f7 | ||
|
|
25c4b676e0 | ||
|
|
6633332e67 | ||
|
|
5928f6709c | ||
|
|
63b2060aef | ||
|
|
24c5a5ac16 | ||
|
|
7f9cc1bd5e |
@@ -1,2 +1,2 @@
|
|||||||
[profile.default]
|
[profile.default]
|
||||||
slow-timeout = { period = "20s", terminate-after = 3 }
|
slow-timeout = { period = "60s", terminate-after = 3 }
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
!scripts/combine_control_files.py
|
!scripts/combine_control_files.py
|
||||||
!scripts/ninstall.sh
|
!scripts/ninstall.sh
|
||||||
!vm-cgconfig.conf
|
!vm-cgconfig.conf
|
||||||
|
!docker-compose/run-tests.sh
|
||||||
|
|
||||||
# Directories
|
# Directories
|
||||||
!.cargo/
|
!.cargo/
|
||||||
@@ -17,11 +18,13 @@
|
|||||||
!libs/
|
!libs/
|
||||||
!neon_local/
|
!neon_local/
|
||||||
!pageserver/
|
!pageserver/
|
||||||
|
!patches/
|
||||||
!pgxn/
|
!pgxn/
|
||||||
!proxy/
|
!proxy/
|
||||||
!s3_scrubber/
|
!storage_scrubber/
|
||||||
!safekeeper/
|
!safekeeper/
|
||||||
!storage_broker/
|
!storage_broker/
|
||||||
|
!storage_controller/
|
||||||
!trace/
|
!trace/
|
||||||
!vendor/postgres-*/
|
!vendor/postgres-*/
|
||||||
!workspace_hack/
|
!workspace_hack/
|
||||||
|
|||||||
5
.github/actionlint.yml
vendored
5
.github/actionlint.yml
vendored
@@ -1,12 +1,11 @@
|
|||||||
self-hosted-runner:
|
self-hosted-runner:
|
||||||
labels:
|
labels:
|
||||||
- arm64
|
- arm64
|
||||||
- dev
|
|
||||||
- gen3
|
- gen3
|
||||||
- large
|
- large
|
||||||
# Remove `macos-14` from the list after https://github.com/rhysd/actionlint/pull/392 is merged.
|
- large-arm64
|
||||||
- macos-14
|
|
||||||
- small
|
- small
|
||||||
|
- small-arm64
|
||||||
- us-east-2
|
- us-east-2
|
||||||
config-variables:
|
config-variables:
|
||||||
- REMOTE_STORAGE_AZURE_CONTAINER
|
- REMOTE_STORAGE_AZURE_CONTAINER
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ runs:
|
|||||||
|
|
||||||
# Use aws s3 cp (instead of aws s3 sync) to keep files from previous runs to make old URLs work,
|
# Use aws s3 cp (instead of aws s3 sync) to keep files from previous runs to make old URLs work,
|
||||||
# and to keep files on the host to upload them to the database
|
# and to keep files on the host to upload them to the database
|
||||||
time aws s3 cp --recursive --only-show-errors "${WORKDIR}/report" "s3://${BUCKET}/${REPORT_PREFIX}/${GITHUB_RUN_ID}"
|
time s5cmd --log error cp "${WORKDIR}/report/*" "s3://${BUCKET}/${REPORT_PREFIX}/${GITHUB_RUN_ID}/"
|
||||||
|
|
||||||
# Generate redirect
|
# Generate redirect
|
||||||
cat <<EOF > ${WORKDIR}/index.html
|
cat <<EOF > ${WORKDIR}/index.html
|
||||||
@@ -183,7 +183,7 @@ runs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Store Allure test stat in the DB (new)
|
- name: Store Allure test stat in the DB (new)
|
||||||
if: ${{ !cancelled() && inputs.store-test-results-into-db == 'true' }}
|
if: ${{ !cancelled() && inputs.store-test-results-into-db == 'true' }}
|
||||||
|
|||||||
2
.github/actions/download/action.yml
vendored
2
.github/actions/download/action.yml
vendored
@@ -26,7 +26,7 @@ runs:
|
|||||||
TARGET: ${{ inputs.path }}
|
TARGET: ${{ inputs.path }}
|
||||||
ARCHIVE: /tmp/downloads/${{ inputs.name }}.tar.zst
|
ARCHIVE: /tmp/downloads/${{ inputs.name }}.tar.zst
|
||||||
SKIP_IF_DOES_NOT_EXIST: ${{ inputs.skip-if-does-not-exist }}
|
SKIP_IF_DOES_NOT_EXIST: ${{ inputs.skip-if-does-not-exist }}
|
||||||
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}', github.run_id, github.run_attempt) }}
|
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}/{2}', github.event.pull_request.head.sha || github.sha, github.run_id, github.run_attempt) }}
|
||||||
run: |
|
run: |
|
||||||
BUCKET=neon-github-public-dev
|
BUCKET=neon-github-public-dev
|
||||||
FILENAME=$(basename $ARCHIVE)
|
FILENAME=$(basename $ARCHIVE)
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ description: 'Create Branch using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
desctiption: 'Neon API key'
|
description: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
project_id:
|
project_id:
|
||||||
desctiption: 'ID of the Project to create Branch in'
|
description: 'ID of the Project to create Branch in'
|
||||||
required: true
|
required: true
|
||||||
api_host:
|
api_host:
|
||||||
desctiption: 'Neon API host'
|
description: 'Neon API host'
|
||||||
default: console.stage.neon.tech
|
default: console-stage.neon.build
|
||||||
outputs:
|
outputs:
|
||||||
dsn:
|
dsn:
|
||||||
description: 'Created Branch DSN (for main database)'
|
description: 'Created Branch DSN (for main database)'
|
||||||
|
|||||||
10
.github/actions/neon-branch-delete/action.yml
vendored
10
.github/actions/neon-branch-delete/action.yml
vendored
@@ -3,17 +3,17 @@ description: 'Delete Branch using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
desctiption: 'Neon API key'
|
description: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
project_id:
|
project_id:
|
||||||
desctiption: 'ID of the Project which should be deleted'
|
description: 'ID of the Project which should be deleted'
|
||||||
required: true
|
required: true
|
||||||
branch_id:
|
branch_id:
|
||||||
desctiption: 'ID of the branch to delete'
|
description: 'ID of the branch to delete'
|
||||||
required: true
|
required: true
|
||||||
api_host:
|
api_host:
|
||||||
desctiption: 'Neon API host'
|
description: 'Neon API host'
|
||||||
default: console.stage.neon.tech
|
default: console-stage.neon.build
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|||||||
16
.github/actions/neon-project-create/action.yml
vendored
16
.github/actions/neon-project-create/action.yml
vendored
@@ -3,22 +3,22 @@ description: 'Create Neon Project using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
desctiption: 'Neon API key'
|
description: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
region_id:
|
region_id:
|
||||||
desctiption: 'Region ID, if not set the project will be created in the default region'
|
description: 'Region ID, if not set the project will be created in the default region'
|
||||||
default: aws-us-east-2
|
default: aws-us-east-2
|
||||||
postgres_version:
|
postgres_version:
|
||||||
desctiption: 'Postgres version; default is 15'
|
description: 'Postgres version; default is 15'
|
||||||
default: 15
|
default: '15'
|
||||||
api_host:
|
api_host:
|
||||||
desctiption: 'Neon API host'
|
description: 'Neon API host'
|
||||||
default: console.stage.neon.tech
|
default: console-stage.neon.build
|
||||||
provisioner:
|
provisioner:
|
||||||
desctiption: 'k8s-pod or k8s-neonvm'
|
description: 'k8s-pod or k8s-neonvm'
|
||||||
default: 'k8s-pod'
|
default: 'k8s-pod'
|
||||||
compute_units:
|
compute_units:
|
||||||
desctiption: '[Min, Max] compute units; Min and Max are used for k8s-neonvm with autoscaling, for k8s-pod values Min and Max should be equal'
|
description: '[Min, Max] compute units; Min and Max are used for k8s-neonvm with autoscaling, for k8s-pod values Min and Max should be equal'
|
||||||
default: '[1, 1]'
|
default: '[1, 1]'
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ description: 'Delete Neon Project using API'
|
|||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
api_key:
|
api_key:
|
||||||
desctiption: 'Neon API key'
|
description: 'Neon API key'
|
||||||
required: true
|
required: true
|
||||||
project_id:
|
project_id:
|
||||||
desctiption: 'ID of the Project to delete'
|
description: 'ID of the Project to delete'
|
||||||
required: true
|
required: true
|
||||||
api_host:
|
api_host:
|
||||||
desctiption: 'Neon API host'
|
description: 'Neon API host'
|
||||||
default: console.stage.neon.tech
|
default: console-stage.neon.build
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: "composite"
|
||||||
|
|||||||
10
.github/actions/run-python-test-set/action.yml
vendored
10
.github/actions/run-python-test-set/action.yml
vendored
@@ -56,14 +56,14 @@ runs:
|
|||||||
if: inputs.build_type != 'remote'
|
if: inputs.build_type != 'remote'
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ inputs.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ inputs.build_type }}-artifact
|
||||||
path: /tmp/neon
|
path: /tmp/neon
|
||||||
|
|
||||||
- name: Download Neon binaries for the previous release
|
- name: Download Neon binaries for the previous release
|
||||||
if: inputs.build_type != 'remote'
|
if: inputs.build_type != 'remote'
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ inputs.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ inputs.build_type }}-artifact
|
||||||
path: /tmp/neon-previous
|
path: /tmp/neon-previous
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ runs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
@@ -183,8 +183,7 @@ runs:
|
|||||||
|
|
||||||
# Run the tests.
|
# Run the tests.
|
||||||
#
|
#
|
||||||
# The junit.xml file allows CI tools to display more fine-grained test information
|
# --alluredir saves test results in Allure format (in a specified directory)
|
||||||
# in its "Tests" tab in the results page.
|
|
||||||
# --verbose prints name of each test (helpful when there are
|
# --verbose prints name of each test (helpful when there are
|
||||||
# multiple tests in one file)
|
# multiple tests in one file)
|
||||||
# -rA prints summary in the end
|
# -rA prints summary in the end
|
||||||
@@ -193,7 +192,6 @@ runs:
|
|||||||
#
|
#
|
||||||
mkdir -p $TEST_OUTPUT/allure/results
|
mkdir -p $TEST_OUTPUT/allure/results
|
||||||
"${cov_prefix[@]}" ./scripts/pytest \
|
"${cov_prefix[@]}" ./scripts/pytest \
|
||||||
--junitxml=$TEST_OUTPUT/junit.xml \
|
|
||||||
--alluredir=$TEST_OUTPUT/allure/results \
|
--alluredir=$TEST_OUTPUT/allure/results \
|
||||||
--tb=short \
|
--tb=short \
|
||||||
--verbose \
|
--verbose \
|
||||||
|
|||||||
4
.github/actions/upload/action.yml
vendored
4
.github/actions/upload/action.yml
vendored
@@ -8,7 +8,7 @@ inputs:
|
|||||||
description: "A directory or file to upload"
|
description: "A directory or file to upload"
|
||||||
required: true
|
required: true
|
||||||
prefix:
|
prefix:
|
||||||
description: "S3 prefix. Default is '${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}'"
|
description: "S3 prefix. Default is '${GITHUB_SHA}/${GITHUB_RUN_ID}/${GITHUB_RUN_ATTEMPT}'"
|
||||||
required: false
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
@@ -45,7 +45,7 @@ runs:
|
|||||||
env:
|
env:
|
||||||
SOURCE: ${{ inputs.path }}
|
SOURCE: ${{ inputs.path }}
|
||||||
ARCHIVE: /tmp/uploads/${{ inputs.name }}.tar.zst
|
ARCHIVE: /tmp/uploads/${{ inputs.name }}.tar.zst
|
||||||
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}', github.run_id, github.run_attempt) }}
|
PREFIX: artifacts/${{ inputs.prefix || format('{0}/{1}/{2}', github.event.pull_request.head.sha || github.sha, github.run_id , github.run_attempt) }}
|
||||||
run: |
|
run: |
|
||||||
BUCKET=neon-github-public-dev
|
BUCKET=neon-github-public-dev
|
||||||
FILENAME=$(basename $ARCHIVE)
|
FILENAME=$(basename $ARCHIVE)
|
||||||
|
|||||||
15
.github/workflows/actionlint.yml
vendored
15
.github/workflows/actionlint.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
actionlint:
|
actionlint:
|
||||||
needs: [ check-permissions ]
|
needs: [ check-permissions ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: reviewdog/action-actionlint@v1
|
- uses: reviewdog/action-actionlint@v1
|
||||||
@@ -36,3 +36,16 @@ jobs:
|
|||||||
fail_on_error: true
|
fail_on_error: true
|
||||||
filter_mode: nofilter
|
filter_mode: nofilter
|
||||||
level: error
|
level: error
|
||||||
|
|
||||||
|
- name: Disallow 'ubuntu-latest' runners
|
||||||
|
run: |
|
||||||
|
PAT='^\s*runs-on:.*-latest'
|
||||||
|
if grep -ERq $PAT .github/workflows; then
|
||||||
|
grep -ERl $PAT .github/workflows |\
|
||||||
|
while read -r f
|
||||||
|
do
|
||||||
|
l=$(grep -nE $PAT .github/workflows/release.yml | awk -F: '{print $1}' | head -1)
|
||||||
|
echo "::error file=$f,line=$l::Please use 'ubuntu-22.04' instead of 'ubuntu-latest'"
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
60
.github/workflows/approved-for-ci-run.yml
vendored
60
.github/workflows/approved-for-ci-run.yml
vendored
@@ -18,6 +18,7 @@ on:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
@@ -43,7 +44,7 @@ jobs:
|
|||||||
contains(fromJSON('["opened", "synchronize", "reopened", "closed"]'), github.event.action) &&
|
contains(fromJSON('["opened", "synchronize", "reopened", "closed"]'), github.event.action) &&
|
||||||
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
||||||
@@ -59,7 +60,7 @@ jobs:
|
|||||||
github.event.action == 'labeled' &&
|
github.event.action == 'labeled' &&
|
||||||
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
contains(github.event.pull_request.labels.*.name, 'approved-for-ci-run')
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
- run: gh pr --repo "${GITHUB_REPOSITORY}" edit "${PR_NUMBER}" --remove-label "approved-for-ci-run"
|
||||||
@@ -68,15 +69,41 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: main
|
ref: main
|
||||||
token: ${{ secrets.CI_ACCESS_TOKEN }}
|
token: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Look for existing PR
|
||||||
|
id: get-pr
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
|
run: |
|
||||||
|
ALREADY_CREATED="$(gh pr --repo ${GITHUB_REPOSITORY} list --head ${BRANCH} --base main --json number --jq '.[].number')"
|
||||||
|
echo "ALREADY_CREATED=${ALREADY_CREATED}" >> ${GITHUB_OUTPUT}
|
||||||
|
|
||||||
|
- name: Get changed labels
|
||||||
|
id: get-labels
|
||||||
|
if: steps.get-pr.outputs.ALREADY_CREATED != ''
|
||||||
|
env:
|
||||||
|
ALREADY_CREATED: ${{ steps.get-pr.outputs.ALREADY_CREATED }}
|
||||||
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
|
run: |
|
||||||
|
LABELS_TO_REMOVE=$(comm -23 <(gh pr --repo ${GITHUB_REPOSITORY} view ${ALREADY_CREATED} --json labels --jq '.labels.[].name'| ( grep -E '^run' || true ) | sort) \
|
||||||
|
<(gh pr --repo ${GITHUB_REPOSITORY} view ${PR_NUMBER} --json labels --jq '.labels.[].name' | ( grep -E '^run' || true ) | sort ) |\
|
||||||
|
( grep -v run-e2e-tests-in-draft || true ) | paste -sd , -)
|
||||||
|
LABELS_TO_ADD=$(comm -13 <(gh pr --repo ${GITHUB_REPOSITORY} view ${ALREADY_CREATED} --json labels --jq '.labels.[].name'| ( grep -E '^run' || true ) |sort) \
|
||||||
|
<(gh pr --repo ${GITHUB_REPOSITORY} view ${PR_NUMBER} --json labels --jq '.labels.[].name' | ( grep -E '^run' || true ) | sort ) |\
|
||||||
|
paste -sd , -)
|
||||||
|
echo "LABELS_TO_ADD=${LABELS_TO_ADD}" >> ${GITHUB_OUTPUT}
|
||||||
|
echo "LABELS_TO_REMOVE=${LABELS_TO_REMOVE}" >> ${GITHUB_OUTPUT}
|
||||||
|
|
||||||
- run: gh pr checkout "${PR_NUMBER}"
|
- run: gh pr checkout "${PR_NUMBER}"
|
||||||
|
|
||||||
- run: git checkout -b "${BRANCH}"
|
- run: git checkout -b "${BRANCH}"
|
||||||
|
|
||||||
- run: git push --force origin "${BRANCH}"
|
- run: git push --force origin "${BRANCH}"
|
||||||
|
if: steps.get-pr.outputs.ALREADY_CREATED == ''
|
||||||
|
|
||||||
- name: Create a Pull Request for CI run (if required)
|
- name: Create a Pull Request for CI run (if required)
|
||||||
env:
|
if: steps.get-pr.outputs.ALREADY_CREATED == ''
|
||||||
|
env:
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
cat << EOF > body.md
|
cat << EOF > body.md
|
||||||
@@ -87,16 +114,33 @@ jobs:
|
|||||||
Feel free to review/comment/discuss the original PR #${PR_NUMBER}.
|
Feel free to review/comment/discuss the original PR #${PR_NUMBER}.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
ALREADY_CREATED="$(gh pr --repo ${GITHUB_REPOSITORY} list --head ${BRANCH} --base main --json number --jq '.[].number')"
|
LABELS=$( (gh pr --repo "${GITHUB_REPOSITORY}" view ${PR_NUMBER} --json labels --jq '.labels.[].name'; echo run-e2e-tests-in-draft )| \
|
||||||
if [ -z "${ALREADY_CREATED}" ]; then
|
grep -E '^run' | paste -sd , -)
|
||||||
gh pr --repo "${GITHUB_REPOSITORY}" create --title "CI run for PR #${PR_NUMBER}" \
|
gh pr --repo "${GITHUB_REPOSITORY}" create --title "CI run for PR #${PR_NUMBER}" \
|
||||||
--body-file "body.md" \
|
--body-file "body.md" \
|
||||||
--head "${BRANCH}" \
|
--head "${BRANCH}" \
|
||||||
--base "main" \
|
--base "main" \
|
||||||
--label "run-e2e-tests-in-draft" \
|
--label ${LABELS} \
|
||||||
--draft
|
--draft
|
||||||
|
- name: Modify the existing pull request (if required)
|
||||||
|
if: steps.get-pr.outputs.ALREADY_CREATED != ''
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
LABELS_TO_ADD: ${{ steps.get-labels.outputs.LABELS_TO_ADD }}
|
||||||
|
LABELS_TO_REMOVE: ${{ steps.get-labels.outputs.LABELS_TO_REMOVE }}
|
||||||
|
ALREADY_CREATED: ${{ steps.get-pr.outputs.ALREADY_CREATED }}
|
||||||
|
run: |
|
||||||
|
ADD_CMD=
|
||||||
|
REMOVE_CMD=
|
||||||
|
[ -z "${LABELS_TO_ADD}" ] || ADD_CMD="--add-label ${LABELS_TO_ADD}"
|
||||||
|
[ -z "${LABELS_TO_REMOVE}" ] || REMOVE_CMD="--remove-label ${LABELS_TO_REMOVE}"
|
||||||
|
if [ -n "${ADD_CMD}" ] || [ -n "${REMOVE_CMD}" ]; then
|
||||||
|
gh pr --repo "${GITHUB_REPOSITORY}" edit ${ALREADY_CREATED} ${ADD_CMD} ${REMOVE_CMD}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- run: git push --force origin "${BRANCH}"
|
||||||
|
if: steps.get-pr.outputs.ALREADY_CREATED != ''
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
# Close PRs and delete branchs if the original PR is closed.
|
# Close PRs and delete branchs if the original PR is closed.
|
||||||
|
|
||||||
@@ -108,7 +152,7 @@ jobs:
|
|||||||
github.event.action == 'closed' &&
|
github.event.action == 'closed' &&
|
||||||
github.event.pull_request.head.repo.full_name != github.repository
|
github.event.pull_request.head.repo.full_name != github.repository
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Close PR and delete `ci-run/pr-${{ env.PR_NUMBER }}` branch
|
- name: Close PR and delete `ci-run/pr-${{ env.PR_NUMBER }}` branch
|
||||||
|
|||||||
172
.github/workflows/benchmarking.yml
vendored
172
.github/workflows/benchmarking.yml
vendored
@@ -38,6 +38,11 @@ on:
|
|||||||
description: 'AWS-RDS and AWS-AURORA normally only run on Saturday. Set this to true to run them on every workflow_dispatch'
|
description: 'AWS-RDS and AWS-AURORA normally only run on Saturday. Set this to true to run them on every workflow_dispatch'
|
||||||
required: false
|
required: false
|
||||||
default: false
|
default: false
|
||||||
|
run_only_pgvector_tests:
|
||||||
|
type: boolean
|
||||||
|
description: 'Run pgvector tests but no other tests. If not set, all tests including pgvector tests will be run'
|
||||||
|
required: false
|
||||||
|
default: false
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
@@ -50,6 +55,7 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
bench:
|
bench:
|
||||||
|
if: ${{ github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null }}
|
||||||
env:
|
env:
|
||||||
TEST_PG_BENCH_DURATIONS_MATRIX: "300"
|
TEST_PG_BENCH_DURATIONS_MATRIX: "300"
|
||||||
TEST_PG_BENCH_SCALES_MATRIX: "10,100"
|
TEST_PG_BENCH_SCALES_MATRIX: "10,100"
|
||||||
@@ -71,7 +77,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -93,7 +99,7 @@ jobs:
|
|||||||
# Set --sparse-ordering option of pytest-order plugin
|
# Set --sparse-ordering option of pytest-order plugin
|
||||||
# to ensure tests are running in order of appears in the file.
|
# to ensure tests are running in order of appears in the file.
|
||||||
# It's important for test_perf_pgbench.py::test_pgbench_remote_* tests
|
# It's important for test_perf_pgbench.py::test_pgbench_remote_* tests
|
||||||
extra_params: -m remote_cluster --sparse-ordering --timeout 5400 --ignore test_runner/performance/test_perf_olap.py
|
extra_params: -m remote_cluster --sparse-ordering --timeout 5400 --ignore test_runner/performance/test_perf_olap.py --ignore test_runner/performance/test_perf_pgvector_queries.py
|
||||||
env:
|
env:
|
||||||
BENCHMARK_CONNSTR: ${{ steps.create-neon-project.outputs.dsn }}
|
BENCHMARK_CONNSTR: ${{ steps.create-neon-project.outputs.dsn }}
|
||||||
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
||||||
@@ -120,6 +126,7 @@ jobs:
|
|||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
generate-matrices:
|
generate-matrices:
|
||||||
|
if: ${{ github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null }}
|
||||||
# Create matrices for the benchmarking jobs, so we run benchmarks on rds only once a week (on Saturday)
|
# Create matrices for the benchmarking jobs, so we run benchmarks on rds only once a week (on Saturday)
|
||||||
#
|
#
|
||||||
# Available platforms:
|
# Available platforms:
|
||||||
@@ -130,7 +137,7 @@ jobs:
|
|||||||
# - rds-postgres: RDS Postgres db.m5.large instance (2 vCPU, 8 GiB) with gp3 EBS storage
|
# - rds-postgres: RDS Postgres db.m5.large instance (2 vCPU, 8 GiB) with gp3 EBS storage
|
||||||
env:
|
env:
|
||||||
RUN_AWS_RDS_AND_AURORA: ${{ github.event.inputs.run_AWS_RDS_AND_AURORA || 'false' }}
|
RUN_AWS_RDS_AND_AURORA: ${{ github.event.inputs.run_AWS_RDS_AND_AURORA || 'false' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
pgbench-compare-matrix: ${{ steps.pgbench-compare-matrix.outputs.matrix }}
|
pgbench-compare-matrix: ${{ steps.pgbench-compare-matrix.outputs.matrix }}
|
||||||
olap-compare-matrix: ${{ steps.olap-compare-matrix.outputs.matrix }}
|
olap-compare-matrix: ${{ steps.olap-compare-matrix.outputs.matrix }}
|
||||||
@@ -147,15 +154,16 @@ jobs:
|
|||||||
"neonvm-captest-new"
|
"neonvm-captest-new"
|
||||||
],
|
],
|
||||||
"db_size": [ "10gb" ],
|
"db_size": [ "10gb" ],
|
||||||
"include": [{ "platform": "neon-captest-freetier", "db_size": "3gb" },
|
"include": [{ "platform": "neon-captest-freetier", "db_size": "3gb" },
|
||||||
{ "platform": "neon-captest-new", "db_size": "50gb" },
|
{ "platform": "neon-captest-new", "db_size": "50gb" },
|
||||||
{ "platform": "neonvm-captest-freetier", "db_size": "3gb" },
|
{ "platform": "neonvm-captest-freetier", "db_size": "3gb" },
|
||||||
{ "platform": "neonvm-captest-new", "db_size": "50gb" }]
|
{ "platform": "neonvm-captest-new", "db_size": "50gb" },
|
||||||
|
{ "platform": "neonvm-captest-sharding-reuse", "db_size": "50gb" }]
|
||||||
}'
|
}'
|
||||||
|
|
||||||
if [ "$(date +%A)" = "Saturday" ]; then
|
if [ "$(date +%A)" = "Saturday" ]; then
|
||||||
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "db_size": "10gb"},
|
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "db_size": "10gb"},
|
||||||
{ "platform": "rds-aurora", "db_size": "50gb"}]')
|
{ "platform": "rds-aurora", "db_size": "50gb"}]')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
||||||
@@ -171,7 +179,7 @@ jobs:
|
|||||||
|
|
||||||
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
||||||
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres" },
|
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres" },
|
||||||
{ "platform": "rds-aurora" }]')
|
{ "platform": "rds-aurora" }]')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
||||||
@@ -190,12 +198,13 @@ jobs:
|
|||||||
|
|
||||||
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
if [ "$(date +%A)" = "Saturday" ] || [ ${RUN_AWS_RDS_AND_AURORA} = "true" ]; then
|
||||||
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "scale": "10" },
|
matrix=$(echo "$matrix" | jq '.include += [{ "platform": "rds-postgres", "scale": "10" },
|
||||||
{ "platform": "rds-aurora", "scale": "10" }]')
|
{ "platform": "rds-aurora", "scale": "10" }]')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
echo "matrix=$(echo "$matrix" | jq --compact-output '.')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
pgbench-compare:
|
pgbench-compare:
|
||||||
|
if: ${{ github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null }}
|
||||||
needs: [ generate-matrices ]
|
needs: [ generate-matrices ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -226,7 +235,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -253,6 +262,9 @@ jobs:
|
|||||||
neon-captest-reuse)
|
neon-captest-reuse)
|
||||||
CONNSTR=${{ secrets.BENCHMARK_CAPTEST_CONNSTR }}
|
CONNSTR=${{ secrets.BENCHMARK_CAPTEST_CONNSTR }}
|
||||||
;;
|
;;
|
||||||
|
neonvm-captest-sharding-reuse)
|
||||||
|
CONNSTR=${{ secrets.BENCHMARK_CAPTEST_SHARDING_CONNSTR }}
|
||||||
|
;;
|
||||||
neon-captest-new | neon-captest-freetier | neonvm-captest-new | neonvm-captest-freetier)
|
neon-captest-new | neon-captest-freetier | neonvm-captest-new | neonvm-captest-freetier)
|
||||||
CONNSTR=${{ steps.create-neon-project.outputs.dsn }}
|
CONNSTR=${{ steps.create-neon-project.outputs.dsn }}
|
||||||
;;
|
;;
|
||||||
@@ -270,11 +282,15 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERY="SELECT version();"
|
QUERIES=("SELECT version()")
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
QUERIES+=("SHOW neon.tenant_id")
|
||||||
|
QUERIES+=("SHOW neon.timeline_id")
|
||||||
fi
|
fi
|
||||||
psql ${CONNSTR} -c "${QUERY}"
|
|
||||||
|
for q in "${QUERIES[@]}"; do
|
||||||
|
psql ${CONNSTR} -c "${q}"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Benchmark init
|
- name: Benchmark init
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
@@ -335,6 +351,92 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
|
pgbench-pgvector:
|
||||||
|
env:
|
||||||
|
TEST_PG_BENCH_DURATIONS_MATRIX: "15m"
|
||||||
|
TEST_PG_BENCH_SCALES_MATRIX: "1"
|
||||||
|
POSTGRES_DISTRIB_DIR: /tmp/neon/pg_install
|
||||||
|
DEFAULT_PG_VERSION: 16
|
||||||
|
TEST_OUTPUT: /tmp/test_output
|
||||||
|
BUILD_TYPE: remote
|
||||||
|
SAVE_PERF_REPORT: ${{ github.event.inputs.save_perf_report || ( github.ref_name == 'main' ) }}
|
||||||
|
PLATFORM: "neon-captest-pgvector"
|
||||||
|
|
||||||
|
runs-on: [ self-hosted, us-east-2, x64 ]
|
||||||
|
container:
|
||||||
|
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/build-tools:pinned
|
||||||
|
options: --init
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download Neon artifact
|
||||||
|
uses: ./.github/actions/download
|
||||||
|
with:
|
||||||
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
|
path: /tmp/neon/
|
||||||
|
prefix: latest
|
||||||
|
|
||||||
|
- name: Add Postgres binaries to PATH
|
||||||
|
run: |
|
||||||
|
${POSTGRES_DISTRIB_DIR}/v${DEFAULT_PG_VERSION}/bin/pgbench --version
|
||||||
|
echo "${POSTGRES_DISTRIB_DIR}/v${DEFAULT_PG_VERSION}/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Set up Connection String
|
||||||
|
id: set-up-connstr
|
||||||
|
run: |
|
||||||
|
CONNSTR=${{ secrets.BENCHMARK_PGVECTOR_CONNSTR }}
|
||||||
|
|
||||||
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
QUERIES=("SELECT version()")
|
||||||
|
QUERIES+=("SHOW neon.tenant_id")
|
||||||
|
QUERIES+=("SHOW neon.timeline_id")
|
||||||
|
|
||||||
|
for q in "${QUERIES[@]}"; do
|
||||||
|
psql ${CONNSTR} -c "${q}"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Benchmark pgvector hnsw indexing
|
||||||
|
uses: ./.github/actions/run-python-test-set
|
||||||
|
with:
|
||||||
|
build_type: ${{ env.BUILD_TYPE }}
|
||||||
|
test_selection: performance/test_perf_olap.py
|
||||||
|
run_in_parallel: false
|
||||||
|
save_perf_report: ${{ env.SAVE_PERF_REPORT }}
|
||||||
|
extra_params: -m remote_cluster --timeout 21600 -k test_pgvector_indexing
|
||||||
|
env:
|
||||||
|
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
||||||
|
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
||||||
|
BENCHMARK_CONNSTR: ${{ steps.set-up-connstr.outputs.connstr }}
|
||||||
|
|
||||||
|
- name: Benchmark pgvector queries
|
||||||
|
uses: ./.github/actions/run-python-test-set
|
||||||
|
with:
|
||||||
|
build_type: ${{ env.BUILD_TYPE }}
|
||||||
|
test_selection: performance/test_perf_pgvector_queries.py
|
||||||
|
run_in_parallel: false
|
||||||
|
save_perf_report: ${{ env.SAVE_PERF_REPORT }}
|
||||||
|
extra_params: -m remote_cluster --timeout 21600
|
||||||
|
env:
|
||||||
|
BENCHMARK_CONNSTR: ${{ steps.set-up-connstr.outputs.connstr }}
|
||||||
|
VIP_VAP_ACCESS_TOKEN: "${{ secrets.VIP_VAP_ACCESS_TOKEN }}"
|
||||||
|
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
||||||
|
|
||||||
|
- name: Create Allure report
|
||||||
|
if: ${{ !cancelled() }}
|
||||||
|
uses: ./.github/actions/allure-report-generate
|
||||||
|
|
||||||
|
- name: Post to a Slack channel
|
||||||
|
if: ${{ github.event.schedule && failure() }}
|
||||||
|
uses: slackapi/slack-github-action@v1
|
||||||
|
with:
|
||||||
|
channel-id: "C033QLM5P7D" # dev-staging-stream
|
||||||
|
slack-message: "Periodic perf testing neon-captest-pgvector: ${{ job.status }}\n${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||||
|
env:
|
||||||
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
clickbench-compare:
|
clickbench-compare:
|
||||||
# ClichBench DB for rds-aurora and rds-Postgres deployed to the same clusters
|
# ClichBench DB for rds-aurora and rds-Postgres deployed to the same clusters
|
||||||
# we use for performance testing in pgbench-compare.
|
# we use for performance testing in pgbench-compare.
|
||||||
@@ -343,7 +445,7 @@ jobs:
|
|||||||
#
|
#
|
||||||
# *_CLICKBENCH_CONNSTR: Genuine ClickBench DB with ~100M rows
|
# *_CLICKBENCH_CONNSTR: Genuine ClickBench DB with ~100M rows
|
||||||
# *_CLICKBENCH_10M_CONNSTR: DB with the first 10M rows of ClickBench DB
|
# *_CLICKBENCH_10M_CONNSTR: DB with the first 10M rows of ClickBench DB
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() && (github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null) }}
|
||||||
needs: [ generate-matrices, pgbench-compare ]
|
needs: [ generate-matrices, pgbench-compare ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -371,7 +473,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -401,11 +503,15 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERY="SELECT version();"
|
QUERIES=("SELECT version()")
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
QUERIES+=("SHOW neon.tenant_id")
|
||||||
|
QUERIES+=("SHOW neon.timeline_id")
|
||||||
fi
|
fi
|
||||||
psql ${CONNSTR} -c "${QUERY}"
|
|
||||||
|
for q in "${QUERIES[@]}"; do
|
||||||
|
psql ${CONNSTR} -c "${q}"
|
||||||
|
done
|
||||||
|
|
||||||
- name: ClickBench benchmark
|
- name: ClickBench benchmark
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
@@ -443,7 +549,7 @@ jobs:
|
|||||||
# We might change it after https://github.com/neondatabase/neon/issues/2900.
|
# We might change it after https://github.com/neondatabase/neon/issues/2900.
|
||||||
#
|
#
|
||||||
# *_TPCH_S10_CONNSTR: DB generated with scale factor 10 (~10 GB)
|
# *_TPCH_S10_CONNSTR: DB generated with scale factor 10 (~10 GB)
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() && (github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null) }}
|
||||||
needs: [ generate-matrices, clickbench-compare ]
|
needs: [ generate-matrices, clickbench-compare ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -470,7 +576,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -507,11 +613,15 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERY="SELECT version();"
|
QUERIES=("SELECT version()")
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
QUERIES+=("SHOW neon.tenant_id")
|
||||||
|
QUERIES+=("SHOW neon.timeline_id")
|
||||||
fi
|
fi
|
||||||
psql ${CONNSTR} -c "${QUERY}"
|
|
||||||
|
for q in "${QUERIES[@]}"; do
|
||||||
|
psql ${CONNSTR} -c "${q}"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Run TPC-H benchmark
|
- name: Run TPC-H benchmark
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
@@ -541,7 +651,7 @@ jobs:
|
|||||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
user-examples-compare:
|
user-examples-compare:
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() && (github.event.inputs.run_only_pgvector_tests == 'false' || github.event.inputs.run_only_pgvector_tests == null) }}
|
||||||
needs: [ generate-matrices, tpch-compare ]
|
needs: [ generate-matrices, tpch-compare ]
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
@@ -567,7 +677,7 @@ jobs:
|
|||||||
- name: Download Neon artifact
|
- name: Download Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-release-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-release-artifact
|
||||||
path: /tmp/neon/
|
path: /tmp/neon/
|
||||||
prefix: latest
|
prefix: latest
|
||||||
|
|
||||||
@@ -597,11 +707,15 @@ jobs:
|
|||||||
|
|
||||||
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
echo "connstr=${CONNSTR}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
QUERY="SELECT version();"
|
QUERIES=("SELECT version()")
|
||||||
if [[ "${PLATFORM}" = "neon"* ]]; then
|
if [[ "${PLATFORM}" = "neon"* ]]; then
|
||||||
QUERY="${QUERY} SHOW neon.tenant_id; SHOW neon.timeline_id;"
|
QUERIES+=("SHOW neon.tenant_id")
|
||||||
|
QUERIES+=("SHOW neon.timeline_id")
|
||||||
fi
|
fi
|
||||||
psql ${CONNSTR} -c "${QUERY}"
|
|
||||||
|
for q in "${QUERIES[@]}"; do
|
||||||
|
psql ${CONNSTR} -c "${q}"
|
||||||
|
done
|
||||||
|
|
||||||
- name: Run user examples
|
- name: Run user examples
|
||||||
uses: ./.github/actions/run-python-test-set
|
uses: ./.github/actions/run-python-test-set
|
||||||
|
|||||||
10
.github/workflows/build-build-tools-image.yml
vendored
10
.github/workflows/build-build-tools-image.yml
vendored
@@ -21,6 +21,7 @@ defaults:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: build-build-tools-image-${{ inputs.image-tag }}
|
group: build-build-tools-image-${{ inputs.image-tag }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
# No permission for GITHUB_TOKEN by default; the **minimal required** set of permissions should be granted in each job.
|
# No permission for GITHUB_TOKEN by default; the **minimal required** set of permissions should be granted in each job.
|
||||||
permissions: {}
|
permissions: {}
|
||||||
@@ -29,7 +30,6 @@ jobs:
|
|||||||
check-image:
|
check-image:
|
||||||
uses: ./.github/workflows/check-build-tools-image.yml
|
uses: ./.github/workflows/check-build-tools-image.yml
|
||||||
|
|
||||||
# This job uses older version of GitHub Actions because it's run on gen2 runners, which don't support node 20 (for newer versions)
|
|
||||||
build-image:
|
build-image:
|
||||||
needs: [ check-image ]
|
needs: [ check-image ]
|
||||||
if: needs.check-image.outputs.found == 'false'
|
if: needs.check-image.outputs.found == 'false'
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
arch: [ x64, arm64 ]
|
arch: [ x64, arm64 ]
|
||||||
|
|
||||||
runs-on: ${{ fromJson(format('["self-hosted", "dev", "{0}"]', matrix.arch)) }}
|
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'large-arm64' || 'large')) }}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_TAG: ${{ inputs.image-tag }}
|
IMAGE_TAG: ${{ inputs.image-tag }}
|
||||||
@@ -54,7 +54,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
||||||
# The default value is ~/.docker
|
# The default value is ~/.docker
|
||||||
@@ -78,7 +78,7 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile.build-tools
|
file: Dockerfile.build-tools
|
||||||
cache-from: type=registry,ref=neondatabase/build-tools:cache-${{ matrix.arch }}
|
cache-from: type=registry,ref=neondatabase/build-tools:cache-${{ matrix.arch }}
|
||||||
cache-to: type=registry,ref=neondatabase/build-tools:cache-${{ matrix.arch }},mode=max
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/build-tools:cache-{0},mode=max', matrix.arch) || '' }}
|
||||||
tags: neondatabase/build-tools:${{ inputs.image-tag }}-${{ matrix.arch }}
|
tags: neondatabase/build-tools:${{ inputs.image-tag }}-${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
- name: Remove custom docker config directory
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
|
|
||||||
merge-images:
|
merge-images:
|
||||||
needs: [ build-image ]
|
needs: [ build-image ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
env:
|
env:
|
||||||
IMAGE_TAG: ${{ inputs.image-tag }}
|
IMAGE_TAG: ${{ inputs.image-tag }}
|
||||||
|
|||||||
444
.github/workflows/build_and_test.yml
vendored
444
.github/workflows/build_and_test.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
cancel-previous-e2e-tests:
|
cancel-previous-e2e-tests:
|
||||||
needs: [ check-permissions ]
|
needs: [ check-permissions ]
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel previous e2e-tests runs for this PR
|
- name: Cancel previous e2e-tests runs for this PR
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
run: ./scripts/pysync
|
run: ./scripts/pysync
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
# !~/.cargo/registry/src
|
# !~/.cargo/registry/src
|
||||||
# ~/.cargo/git/
|
# ~/.cargo/git/
|
||||||
# target/
|
# target/
|
||||||
# key: v1-${{ runner.os }}-cargo-clippy-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
# key: v1-${{ runner.os }}-${{ runner.arch }}-cargo-clippy-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
|
||||||
# Some of our rust modules use FFI and need those to be checked
|
# Some of our rust modules use FFI and need those to be checked
|
||||||
- name: Get postgres headers
|
- name: Get postgres headers
|
||||||
@@ -236,27 +236,6 @@ jobs:
|
|||||||
submodules: true
|
submodules: true
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Check Postgres submodules revision
|
|
||||||
shell: bash -euo pipefail {0}
|
|
||||||
run: |
|
|
||||||
# This is a temporary solution to ensure that the Postgres submodules revision is correct (i.e. the updated intentionally).
|
|
||||||
# Eventually it will be replaced by a regression test https://github.com/neondatabase/neon/pull/4603
|
|
||||||
|
|
||||||
FAILED=false
|
|
||||||
for postgres in postgres-v14 postgres-v15 postgres-v16; do
|
|
||||||
expected=$(cat vendor/revisions.json | jq --raw-output '."'"${postgres}"'"')
|
|
||||||
actual=$(git rev-parse "HEAD:vendor/${postgres}")
|
|
||||||
if [ "${expected}" != "${actual}" ]; then
|
|
||||||
echo >&2 "Expected ${postgres} rev to be at '${expected}', but it is at '${actual}'"
|
|
||||||
FAILED=true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "${FAILED}" = "true" ]; then
|
|
||||||
echo >&2 "Please update vendor/revisions.json if these changes are intentional"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Set pg 14 revision for caching
|
- name: Set pg 14 revision for caching
|
||||||
id: pg_v14_rev
|
id: pg_v14_rev
|
||||||
run: echo pg_rev=$(git rev-parse HEAD:vendor/postgres-v14) >> $GITHUB_OUTPUT
|
run: echo pg_rev=$(git rev-parse HEAD:vendor/postgres-v14) >> $GITHUB_OUTPUT
|
||||||
@@ -312,29 +291,29 @@ jobs:
|
|||||||
# target/
|
# target/
|
||||||
# # Fall back to older versions of the key, if no cache for current Cargo.lock was found
|
# # Fall back to older versions of the key, if no cache for current Cargo.lock was found
|
||||||
# key: |
|
# key: |
|
||||||
# v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
# v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-${{ hashFiles('Cargo.lock') }}
|
||||||
# v1-${{ runner.os }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-
|
# v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-cargo-${{ hashFiles('rust-toolchain.toml') }}-
|
||||||
|
|
||||||
- name: Cache postgres v14 build
|
- name: Cache postgres v14 build
|
||||||
id: cache_pg_14
|
id: cache_pg_14
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v14
|
path: pg_install/v14
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v14_rev.outputs.pg_rev }}-${{ hashFiles('Makefile') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-pg-${{ steps.pg_v14_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
||||||
|
|
||||||
- name: Cache postgres v15 build
|
- name: Cache postgres v15 build
|
||||||
id: cache_pg_15
|
id: cache_pg_15
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v15
|
path: pg_install/v15
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v15_rev.outputs.pg_rev }}-${{ hashFiles('Makefile') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-pg-${{ steps.pg_v15_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
||||||
|
|
||||||
- name: Cache postgres v16 build
|
- name: Cache postgres v16 build
|
||||||
id: cache_pg_16
|
id: cache_pg_16
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: pg_install/v16
|
path: pg_install/v16
|
||||||
key: v1-${{ runner.os }}-${{ matrix.build_type }}-pg-${{ steps.pg_v16_rev.outputs.pg_rev }}-${{ hashFiles('Makefile') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-pg-${{ steps.pg_v16_rev.outputs.pg_rev }}-${{ hashFiles('Makefile', 'Dockerfile.build-tools') }}
|
||||||
|
|
||||||
- name: Build postgres v14
|
- name: Build postgres v14
|
||||||
if: steps.cache_pg_14.outputs.cache-hit != 'true'
|
if: steps.cache_pg_14.outputs.cache-hit != 'true'
|
||||||
@@ -358,31 +337,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
${cov_prefix} mold -run cargo build $CARGO_FLAGS $CARGO_FEATURES --bins --tests
|
${cov_prefix} mold -run cargo build $CARGO_FLAGS $CARGO_FEATURES --bins --tests
|
||||||
|
|
||||||
- name: Run rust tests
|
# Do install *before* running rust tests because they might recompile the
|
||||||
env:
|
# binaries with different features/flags.
|
||||||
NEXTEST_RETRIES: 3
|
|
||||||
run: |
|
|
||||||
for io_engine in std-fs tokio-epoll-uring ; do
|
|
||||||
NEON_PAGESERVER_UNIT_TEST_VIRTUAL_FILE_IOENGINE=$io_engine ${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES
|
|
||||||
done
|
|
||||||
|
|
||||||
# Run separate tests for real S3
|
|
||||||
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
|
||||||
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
|
||||||
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
|
||||||
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
|
||||||
${cov_prefix} cargo nextest run $CARGO_FLAGS -E 'package(remote_storage)' -E 'test(test_real_s3)'
|
|
||||||
|
|
||||||
# Run separate tests for real Azure Blob Storage
|
|
||||||
# XXX: replace region with `eu-central-1`-like region
|
|
||||||
export ENABLE_REAL_AZURE_REMOTE_STORAGE=y
|
|
||||||
export AZURE_STORAGE_ACCOUNT="${{ secrets.AZURE_STORAGE_ACCOUNT_DEV }}"
|
|
||||||
export AZURE_STORAGE_ACCESS_KEY="${{ secrets.AZURE_STORAGE_ACCESS_KEY_DEV }}"
|
|
||||||
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
|
||||||
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
|
||||||
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
|
||||||
${cov_prefix} cargo nextest run $CARGO_FLAGS -E 'package(remote_storage)' -E 'test(test_real_azure)'
|
|
||||||
|
|
||||||
- name: Install rust binaries
|
- name: Install rust binaries
|
||||||
run: |
|
run: |
|
||||||
# Install target binaries
|
# Install target binaries
|
||||||
@@ -423,13 +379,39 @@ jobs:
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
- name: Run rust tests
|
||||||
|
env:
|
||||||
|
NEXTEST_RETRIES: 3
|
||||||
|
run: |
|
||||||
|
#nextest does not yet support running doctests
|
||||||
|
cargo test --doc $CARGO_FLAGS $CARGO_FEATURES
|
||||||
|
|
||||||
|
for io_engine in std-fs tokio-epoll-uring ; do
|
||||||
|
NEON_PAGESERVER_UNIT_TEST_VIRTUAL_FILE_IOENGINE=$io_engine ${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run separate tests for real S3
|
||||||
|
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
||||||
|
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
||||||
|
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
||||||
|
${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES -E 'package(remote_storage)' -E 'test(test_real_s3)'
|
||||||
|
|
||||||
|
# Run separate tests for real Azure Blob Storage
|
||||||
|
# XXX: replace region with `eu-central-1`-like region
|
||||||
|
export ENABLE_REAL_AZURE_REMOTE_STORAGE=y
|
||||||
|
export AZURE_STORAGE_ACCOUNT="${{ secrets.AZURE_STORAGE_ACCOUNT_DEV }}"
|
||||||
|
export AZURE_STORAGE_ACCESS_KEY="${{ secrets.AZURE_STORAGE_ACCESS_KEY_DEV }}"
|
||||||
|
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
||||||
|
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
||||||
|
${cov_prefix} cargo nextest run $CARGO_FLAGS $CARGO_FEATURES -E 'package(remote_storage)' -E 'test(test_real_azure)'
|
||||||
|
|
||||||
- name: Install postgres binaries
|
- name: Install postgres binaries
|
||||||
run: cp -a pg_install /tmp/neon/pg_install
|
run: cp -a pg_install /tmp/neon/pg_install
|
||||||
|
|
||||||
- name: Upload Neon artifact
|
- name: Upload Neon artifact
|
||||||
uses: ./.github/actions/upload
|
uses: ./.github/actions/upload
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ matrix.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-artifact
|
||||||
path: /tmp/neon
|
path: /tmp/neon
|
||||||
|
|
||||||
# XXX: keep this after the binaries.list is formed, so the coverage can properly work later
|
# XXX: keep this after the binaries.list is formed, so the coverage can properly work later
|
||||||
@@ -477,6 +459,8 @@ jobs:
|
|||||||
BUILD_TAG: ${{ needs.tag.outputs.build-tag }}
|
BUILD_TAG: ${{ needs.tag.outputs.build-tag }}
|
||||||
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
||||||
PAGESERVER_GET_VECTORED_IMPL: vectored
|
PAGESERVER_GET_VECTORED_IMPL: vectored
|
||||||
|
PAGESERVER_GET_IMPL: vectored
|
||||||
|
PAGESERVER_VALIDATE_VEC_GET: true
|
||||||
|
|
||||||
# Temporary disable this step until we figure out why it's so flaky
|
# Temporary disable this step until we figure out why it's so flaky
|
||||||
# Ref https://github.com/neondatabase/neon/issues/4540
|
# Ref https://github.com/neondatabase/neon/issues/4540
|
||||||
@@ -506,7 +490,7 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v1-${{ runner.os }}-python-deps-${{ hashFiles('poetry.lock') }}
|
key: v1-${{ runner.os }}-${{ runner.arch }}-python-deps-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
run: ./scripts/pysync
|
run: ./scripts/pysync
|
||||||
@@ -556,12 +540,33 @@ jobs:
|
|||||||
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
PERF_TEST_RESULT_CONNSTR: "${{ secrets.PERF_TEST_RESULT_CONNSTR }}"
|
||||||
TEST_RESULT_CONNSTR: "${{ secrets.REGRESS_TEST_RESULT_CONNSTR_NEW }}"
|
TEST_RESULT_CONNSTR: "${{ secrets.REGRESS_TEST_RESULT_CONNSTR_NEW }}"
|
||||||
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
PAGESERVER_VIRTUAL_FILE_IO_ENGINE: tokio-epoll-uring
|
||||||
|
PAGESERVER_GET_VECTORED_IMPL: vectored
|
||||||
|
PAGESERVER_GET_IMPL: vectored
|
||||||
|
PAGESERVER_VALIDATE_VEC_GET: false
|
||||||
# XXX: no coverage data handling here, since benchmarks are run on release builds,
|
# XXX: no coverage data handling here, since benchmarks are run on release builds,
|
||||||
# while coverage is currently collected for the debug ones
|
# while coverage is currently collected for the debug ones
|
||||||
|
|
||||||
|
report-benchmarks-failures:
|
||||||
|
needs: [ benchmarks, create-test-report ]
|
||||||
|
if: github.ref_name == 'main' && failure() && needs.benchmarks.result == 'failure'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: slackapi/slack-github-action@v1
|
||||||
|
with:
|
||||||
|
channel-id: C060CNA47S9 # on-call-staging-storage-stream
|
||||||
|
slack-message: |
|
||||||
|
Benchmarks failed on main: ${{ github.event.head_commit.url }}
|
||||||
|
|
||||||
|
Allure report: ${{ needs.create-test-report.outputs.report-url }}
|
||||||
|
env:
|
||||||
|
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
|
||||||
|
|
||||||
create-test-report:
|
create-test-report:
|
||||||
needs: [ check-permissions, regress-tests, coverage-report, benchmarks, build-build-tools-image ]
|
needs: [ check-permissions, regress-tests, coverage-report, benchmarks, build-build-tools-image ]
|
||||||
if: ${{ !cancelled() && contains(fromJSON('["skipped", "success"]'), needs.check-permissions.result) }}
|
if: ${{ !cancelled() && contains(fromJSON('["skipped", "success"]'), needs.check-permissions.result) }}
|
||||||
|
outputs:
|
||||||
|
report-url: ${{ steps.create-allure-report.outputs.report-url }}
|
||||||
|
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: [ self-hosted, gen3, small ]
|
||||||
container:
|
container:
|
||||||
@@ -634,7 +639,7 @@ jobs:
|
|||||||
- name: Get Neon artifact
|
- name: Get Neon artifact
|
||||||
uses: ./.github/actions/download
|
uses: ./.github/actions/download
|
||||||
with:
|
with:
|
||||||
name: neon-${{ runner.os }}-${{ matrix.build_type }}-artifact
|
name: neon-${{ runner.os }}-${{ runner.arch }}-${{ matrix.build_type }}-artifact
|
||||||
path: /tmp/neon
|
path: /tmp/neon
|
||||||
|
|
||||||
- name: Get coverage artifact
|
- name: Get coverage artifact
|
||||||
@@ -718,9 +723,13 @@ jobs:
|
|||||||
uses: ./.github/workflows/trigger-e2e-tests.yml
|
uses: ./.github/workflows/trigger-e2e-tests.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
neon-image:
|
neon-image-arch:
|
||||||
needs: [ check-permissions, build-build-tools-image, tag ]
|
needs: [ check-permissions, build-build-tools-image, tag ]
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
strategy:
|
||||||
|
matrix:
|
||||||
|
arch: [ x64, arm64 ]
|
||||||
|
|
||||||
|
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'large-arm64' || 'large')) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -735,19 +744,13 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p .docker-custom
|
mkdir -p .docker-custom
|
||||||
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
- uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
|
||||||
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
|
||||||
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
|
||||||
|
|
||||||
- uses: docker/build-push-action@v5
|
- uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
@@ -759,25 +762,52 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile
|
file: Dockerfile
|
||||||
cache-from: type=registry,ref=neondatabase/neon:cache
|
cache-from: type=registry,ref=neondatabase/neon:cache-${{ matrix.arch }}
|
||||||
cache-to: type=registry,ref=neondatabase/neon:cache,mode=max
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/neon:cache-{0},mode=max', matrix.arch) || '' }}
|
||||||
tags: |
|
tags: |
|
||||||
369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}}
|
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
||||||
neondatabase/neon:${{needs.tag.outputs.build-tag}}
|
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
- name: Remove custom docker config directory
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
rm -rf .docker-custom
|
rm -rf .docker-custom
|
||||||
|
|
||||||
compute-node-image:
|
neon-image:
|
||||||
needs: [ check-permissions, build-build-tools-image, tag ]
|
needs: [ neon-image-arch, tag ]
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Create multi-arch image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t neondatabase/neon:${{ needs.tag.outputs.build-tag }} \
|
||||||
|
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-x64 \
|
||||||
|
neondatabase/neon:${{ needs.tag.outputs.build-tag }}-arm64
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
|
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
||||||
|
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
||||||
|
|
||||||
|
- name: Push multi-arch image to ECR
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{ needs.tag.outputs.build-tag }} \
|
||||||
|
neondatabase/neon:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
|
compute-node-image-arch:
|
||||||
|
needs: [ check-permissions, build-build-tools-image, tag ]
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
version: [ v14, v15, v16 ]
|
version: [ v14, v15, v16 ]
|
||||||
|
arch: [ x64, arm64 ]
|
||||||
|
|
||||||
|
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'large-arm64' || 'large')) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -792,7 +822,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mkdir -p .docker-custom
|
mkdir -p .docker-custom
|
||||||
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-buildx-action@v2
|
||||||
with:
|
with:
|
||||||
# Disable parallelism for docker buildkit.
|
# Disable parallelism for docker buildkit.
|
||||||
# As we already build everything with `make -j$(nproc)`, running it in additional level of parallelisam blows up the Runner.
|
# As we already build everything with `make -j$(nproc)`, running it in additional level of parallelisam blows up the Runner.
|
||||||
@@ -824,15 +854,34 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile.compute-node
|
file: Dockerfile.compute-node
|
||||||
cache-from: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache
|
cache-from: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache-${{ matrix.arch }}
|
||||||
cache-to: type=registry,ref=neondatabase/compute-node-${{ matrix.version }}:cache,mode=max
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/compute-node-{0}:cache-{1},mode=max', matrix.version, matrix.arch) || '' }}
|
||||||
tags: |
|
tags: |
|
||||||
369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
||||||
neondatabase/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
|
||||||
|
- name: Build neon extensions test image
|
||||||
|
if: matrix.version == 'v16'
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
build-args: |
|
||||||
|
GIT_VERSION=${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
PG_VERSION=${{ matrix.version }}
|
||||||
|
BUILD_TAG=${{ needs.tag.outputs.build-tag }}
|
||||||
|
TAG=${{ needs.build-build-tools-image.outputs.image-tag }}
|
||||||
|
provenance: false
|
||||||
|
push: true
|
||||||
|
pull: true
|
||||||
|
file: Dockerfile.compute-node
|
||||||
|
target: neon-pg-ext-test
|
||||||
|
cache-from: type=registry,ref=neondatabase/neon-test-extensions-${{ matrix.version }}:cache-${{ matrix.arch }}
|
||||||
|
cache-to: ${{ github.ref_name == 'main' && format('type=registry,ref=neondatabase/neon-test-extensions-{0}:cache-{1},mode=max', matrix.version, matrix.arch) || '' }}
|
||||||
|
tags: |
|
||||||
|
neondatabase/neon-test-extensions-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}-${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Build compute-tools image
|
- name: Build compute-tools image
|
||||||
# compute-tools are Postgres independent, so build it only once
|
# compute-tools are Postgres independent, so build it only once
|
||||||
if: ${{ matrix.version == 'v16' }}
|
if: matrix.version == 'v16'
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
target: compute-tools-image
|
target: compute-tools-image
|
||||||
@@ -846,14 +895,64 @@ jobs:
|
|||||||
pull: true
|
pull: true
|
||||||
file: Dockerfile.compute-node
|
file: Dockerfile.compute-node
|
||||||
tags: |
|
tags: |
|
||||||
369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{ needs.tag.outputs.build-tag }}
|
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}-${{ matrix.arch }}
|
||||||
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}
|
|
||||||
|
|
||||||
- name: Remove custom docker config directory
|
- name: Remove custom docker config directory
|
||||||
if: always()
|
if: always()
|
||||||
run: |
|
run: |
|
||||||
rm -rf .docker-custom
|
rm -rf .docker-custom
|
||||||
|
|
||||||
|
compute-node-image:
|
||||||
|
needs: [ compute-node-image-arch, tag ]
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
version: [ v14, v15, v16 ]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Create multi-arch compute-node image
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
||||||
|
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-x64 \
|
||||||
|
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-arm64
|
||||||
|
|
||||||
|
- name: Create multi-arch neon-test-extensions image
|
||||||
|
if: matrix.version == 'v16'
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t neondatabase/neon-test-extensions-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
||||||
|
neondatabase/neon-test-extensions-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-x64 \
|
||||||
|
neondatabase/neon-test-extensions-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}-arm64
|
||||||
|
|
||||||
|
- name: Create multi-arch compute-tools image
|
||||||
|
if: matrix.version == 'v16'
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }} \
|
||||||
|
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}-x64 \
|
||||||
|
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}-arm64
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
|
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
||||||
|
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
||||||
|
|
||||||
|
- name: Push multi-arch compute-node-${{ matrix.version }} image to ECR
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
||||||
|
neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
|
- name: Push multi-arch compute-tools image to ECR
|
||||||
|
if: matrix.version == 'v16'
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{ needs.tag.outputs.build-tag }} \
|
||||||
|
neondatabase/compute-tools:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
vm-compute-node-image:
|
vm-compute-node-image:
|
||||||
needs: [ check-permissions, tag, compute-node-image ]
|
needs: [ check-permissions, tag, compute-node-image ]
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, gen3, large ]
|
||||||
@@ -861,15 +960,12 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
version: [ v14, v15, v16 ]
|
version: [ v14, v15, v16 ]
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
shell: sh -eu {0}
|
|
||||||
env:
|
env:
|
||||||
VM_BUILDER_VERSION: v0.23.2
|
VM_BUILDER_VERSION: v0.29.3
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -878,26 +974,48 @@ jobs:
|
|||||||
curl -fL https://github.com/neondatabase/autoscaling/releases/download/$VM_BUILDER_VERSION/vm-builder -o vm-builder
|
curl -fL https://github.com/neondatabase/autoscaling/releases/download/$VM_BUILDER_VERSION/vm-builder -o vm-builder
|
||||||
chmod +x vm-builder
|
chmod +x vm-builder
|
||||||
|
|
||||||
|
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
||||||
|
# The default value is ~/.docker
|
||||||
|
- name: Set custom docker config directory
|
||||||
|
run: |
|
||||||
|
mkdir -p .docker-custom
|
||||||
|
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
# Note: we need a separate pull step here because otherwise vm-builder will try to pull, and
|
# Note: we need a separate pull step here because otherwise vm-builder will try to pull, and
|
||||||
# it won't have the proper authentication (written at v0.6.0)
|
# it won't have the proper authentication (written at v0.6.0)
|
||||||
- name: Pulling compute-node image
|
- name: Pulling compute-node image
|
||||||
run: |
|
run: |
|
||||||
docker pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
docker pull neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
- name: Build vm image
|
- name: Build vm image
|
||||||
run: |
|
run: |
|
||||||
./vm-builder \
|
./vm-builder \
|
||||||
-spec=vm-image-spec.yaml \
|
-spec=vm-image-spec.yaml \
|
||||||
-src=369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}} \
|
-src=neondatabase/compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }} \
|
||||||
-dst=369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
-dst=neondatabase/vm-compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
- name: Pushing vm-compute-node image
|
- name: Pushing vm-compute-node image
|
||||||
run: |
|
run: |
|
||||||
docker push 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${{ matrix.version }}:${{needs.tag.outputs.build-tag}}
|
docker push neondatabase/vm-compute-node-${{ matrix.version }}:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
|
- name: Remove custom docker config directory
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -rf .docker-custom
|
||||||
|
|
||||||
test-images:
|
test-images:
|
||||||
needs: [ check-permissions, tag, neon-image, compute-node-image ]
|
needs: [ check-permissions, tag, neon-image, compute-node-image ]
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
arch: [ x64, arm64 ]
|
||||||
|
|
||||||
|
runs-on: ${{ fromJson(format('["self-hosted", "gen3", "{0}"]', matrix.arch == 'arm64' && 'small-arm64' || 'small')) }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -905,6 +1023,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
# Use custom DOCKER_CONFIG directory to avoid conflicts with default settings
|
||||||
|
# The default value is ~/.docker
|
||||||
|
- name: Set custom docker config directory
|
||||||
|
run: |
|
||||||
|
mkdir -p .docker-custom
|
||||||
|
echo DOCKER_CONFIG=$(pwd)/.docker-custom >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
# `neondatabase/neon` contains multiple binaries, all of them use the same input for the version into the same version formatting library.
|
# `neondatabase/neon` contains multiple binaries, all of them use the same input for the version into the same version formatting library.
|
||||||
# Pick pageserver as currently the only binary with extra "version" features printed in the string to verify.
|
# Pick pageserver as currently the only binary with extra "version" features printed in the string to verify.
|
||||||
# Regular pageserver version string looks like
|
# Regular pageserver version string looks like
|
||||||
@@ -915,7 +1045,7 @@ jobs:
|
|||||||
- name: Verify image versions
|
- name: Verify image versions
|
||||||
shell: bash # ensure no set -e for better error messages
|
shell: bash # ensure no set -e for better error messages
|
||||||
run: |
|
run: |
|
||||||
pageserver_version=$(docker run --rm 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} "/bin/sh" "-c" "/usr/local/bin/pageserver --version")
|
pageserver_version=$(docker run --rm neondatabase/neon:${{ needs.tag.outputs.build-tag }} "/bin/sh" "-c" "/usr/local/bin/pageserver --version")
|
||||||
|
|
||||||
echo "Pageserver version string: $pageserver_version"
|
echo "Pageserver version string: $pageserver_version"
|
||||||
|
|
||||||
@@ -929,7 +1059,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Verify docker-compose example
|
- name: Verify docker-compose example and test extensions
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
run: env TAG=${{needs.tag.outputs.build-tag}} ./docker-compose/docker_compose_test.sh
|
run: env TAG=${{needs.tag.outputs.build-tag}} ./docker-compose/docker_compose_test.sh
|
||||||
|
|
||||||
@@ -939,84 +1069,78 @@ jobs:
|
|||||||
docker compose -f ./docker-compose/docker-compose.yml logs || 0
|
docker compose -f ./docker-compose/docker-compose.yml logs || 0
|
||||||
docker compose -f ./docker-compose/docker-compose.yml down
|
docker compose -f ./docker-compose/docker-compose.yml down
|
||||||
|
|
||||||
|
- name: Remove custom docker config directory
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
rm -rf .docker-custom
|
||||||
|
|
||||||
promote-images:
|
promote-images:
|
||||||
needs: [ check-permissions, tag, test-images, vm-compute-node-image ]
|
needs: [ check-permissions, tag, test-images, vm-compute-node-image ]
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: ubuntu-22.04
|
||||||
container: golang:1.19-bullseye
|
|
||||||
# Don't add if-condition here.
|
env:
|
||||||
# The job should always be run because we have dependant other jobs that shouldn't be skipped
|
VERSIONS: v14 v15 v16
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Install Crane & ECR helper
|
- uses: docker/login-action@v3
|
||||||
run: |
|
with:
|
||||||
go install github.com/google/go-containerregistry/cmd/crane@31786c6cbb82d6ec4fb8eb79cd9387905130534e # v0.11.0
|
username: ${{ secrets.NEON_DOCKERHUB_USERNAME }}
|
||||||
go install github.com/awslabs/amazon-ecr-credential-helper/ecr-login/cli/docker-credential-ecr-login@69c85dc22db6511932bbf119e1a0cc5c90c69a7f # v0.6.0
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
- name: Configure ECR login
|
- name: Login to dev ECR
|
||||||
run: |
|
uses: docker/login-action@v3
|
||||||
mkdir /github/home/.docker/
|
with:
|
||||||
echo "{\"credsStore\":\"ecr-login\"}" > /github/home/.docker/config.json
|
registry: 369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
|
username: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
||||||
|
password: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
||||||
|
|
||||||
- name: Copy vm-compute-node images to Docker Hub
|
- name: Copy vm-compute-node images to ECR
|
||||||
run: |
|
run: |
|
||||||
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} vm-compute-node-v14
|
for version in ${VERSIONS}; do
|
||||||
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} vm-compute-node-v15
|
docker buildx imagetools create -t 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-${version}:${{ needs.tag.outputs.build-tag }} \
|
||||||
crane pull 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} vm-compute-node-v16
|
neondatabase/vm-compute-node-${version}:${{ needs.tag.outputs.build-tag }}
|
||||||
|
done
|
||||||
|
|
||||||
- name: Add latest tag to images
|
- name: Add latest tag to images
|
||||||
if: github.ref_name == 'main' || github.ref_name == 'release' || github.ref_name == 'release-proxy'
|
if: github.ref_name == 'main'
|
||||||
run: |
|
run: |
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} latest
|
for repo in neondatabase 369495373322.dkr.ecr.eu-central-1.amazonaws.com; do
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
docker buildx imagetools create -t $repo/neon:latest \
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
$repo/neon:${{ needs.tag.outputs.build-tag }}
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
|
|
||||||
- name: Push images to production ECR
|
docker buildx imagetools create -t $repo/compute-tools:latest \
|
||||||
if: github.ref_name == 'main' || github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
$repo/compute-tools:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
|
for version in ${VERSIONS}; do
|
||||||
|
docker buildx imagetools create -t $repo/compute-node-${version}:latest \
|
||||||
|
$repo/compute-node-${version}:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
|
docker buildx imagetools create -t $repo/vm-compute-node-${version}:latest \
|
||||||
|
$repo/vm-compute-node-${version}:${{ needs.tag.outputs.build-tag }}
|
||||||
|
done
|
||||||
|
done
|
||||||
|
docker buildx imagetools create -t neondatabase/neon-test-extensions-v16:latest \
|
||||||
|
neondatabase/neon-test-extensions-v16:${{ needs.tag.outputs.build-tag }}
|
||||||
|
|
||||||
|
- name: Login to prod ECR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
if: github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
||||||
|
with:
|
||||||
|
registry: 093970136003.dkr.ecr.eu-central-1.amazonaws.com
|
||||||
|
username: ${{ secrets.PROD_GHA_RUNNER_LIMITED_AWS_ACCESS_KEY_ID }}
|
||||||
|
password: ${{ secrets.PROD_GHA_RUNNER_LIMITED_AWS_SECRET_ACCESS_KEY }}
|
||||||
|
|
||||||
|
- name: Copy all images to prod ECR
|
||||||
|
if: github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
||||||
run: |
|
run: |
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/neon:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/neon:latest
|
for image in neon compute-tools {vm-,}compute-node-{v14,v15,v16}; do
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-tools:latest
|
docker buildx imagetools create -t 093970136003.dkr.ecr.eu-central-1.amazonaws.com/${image}:${{ needs.tag.outputs.build-tag }} \
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v14:latest
|
369495373322.dkr.ecr.eu-central-1.amazonaws.com/${image}:${{ needs.tag.outputs.build-tag }}
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v14:latest
|
done
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v15:latest
|
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v15:latest
|
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v16:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/compute-node-v16:latest
|
|
||||||
crane copy 369495373322.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} 093970136003.dkr.ecr.eu-central-1.amazonaws.com/vm-compute-node-v16:latest
|
|
||||||
|
|
||||||
- name: Configure Docker Hub login
|
|
||||||
run: |
|
|
||||||
# ECR Credential Helper & Docker Hub don't work together in config, hence reset
|
|
||||||
echo "" > /github/home/.docker/config.json
|
|
||||||
crane auth login -u ${{ secrets.NEON_DOCKERHUB_USERNAME }} -p ${{ secrets.NEON_DOCKERHUB_PASSWORD }} index.docker.io
|
|
||||||
|
|
||||||
- name: Push vm-compute-node to Docker Hub
|
|
||||||
run: |
|
|
||||||
crane push vm-compute-node-v14 neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}}
|
|
||||||
crane push vm-compute-node-v15 neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}}
|
|
||||||
crane push vm-compute-node-v16 neondatabase/vm-compute-node-v16:${{needs.tag.outputs.build-tag}}
|
|
||||||
|
|
||||||
- name: Push latest tags to Docker Hub
|
|
||||||
if: github.ref_name == 'main' || github.ref_name == 'release'|| github.ref_name == 'release-proxy'
|
|
||||||
run: |
|
|
||||||
crane tag neondatabase/neon:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag neondatabase/compute-tools:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag neondatabase/compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag neondatabase/vm-compute-node-v14:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag neondatabase/compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag neondatabase/vm-compute-node-v15:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag neondatabase/compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
crane tag neondatabase/vm-compute-node-v16:${{needs.tag.outputs.build-tag}} latest
|
|
||||||
|
|
||||||
- name: Cleanup ECR folder
|
|
||||||
run: rm -rf ~/.ecr
|
|
||||||
|
|
||||||
trigger-custom-extensions-build-and-wait:
|
trigger-custom-extensions-build-and-wait:
|
||||||
needs: [ check-permissions, tag ]
|
needs: [ check-permissions, tag ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Set PR's status to pending and request a remote CI test
|
- name: Set PR's status to pending and request a remote CI test
|
||||||
run: |
|
run: |
|
||||||
@@ -1121,21 +1245,22 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
if [[ "$GITHUB_REF_NAME" == "main" ]]; then
|
||||||
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main -f branch=main -f dockerTag=${{needs.tag.outputs.build-tag}} -f deployPreprodRegion=false
|
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main -f branch=main -f dockerTag=${{needs.tag.outputs.build-tag}} -f deployPreprodRegion=false
|
||||||
|
gh workflow --repo neondatabase/azure run deploy.yml -f dockerTag=${{needs.tag.outputs.build-tag}}
|
||||||
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
elif [[ "$GITHUB_REF_NAME" == "release" ]]; then
|
||||||
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main \
|
gh workflow --repo neondatabase/aws run deploy-dev.yml --ref main \
|
||||||
-f deployPgSniRouter=false \
|
-f deployPgSniRouter=false \
|
||||||
-f deployProxy=false \
|
-f deployProxy=false \
|
||||||
-f deployStorage=true \
|
-f deployStorage=true \
|
||||||
-f deployStorageBroker=true \
|
-f deployStorageBroker=true \
|
||||||
|
-f deployStorageController=true \
|
||||||
-f branch=main \
|
-f branch=main \
|
||||||
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
||||||
-f deployPreprodRegion=true
|
-f deployPreprodRegion=true
|
||||||
|
|
||||||
gh workflow --repo neondatabase/aws run deploy-prod.yml --ref main \
|
gh workflow --repo neondatabase/aws run deploy-prod.yml --ref main \
|
||||||
-f deployPgSniRouter=false \
|
|
||||||
-f deployProxy=false \
|
|
||||||
-f deployStorage=true \
|
-f deployStorage=true \
|
||||||
-f deployStorageBroker=true \
|
-f deployStorageBroker=true \
|
||||||
|
-f deployStorageController=true \
|
||||||
-f branch=main \
|
-f branch=main \
|
||||||
-f dockerTag=${{needs.tag.outputs.build-tag}}
|
-f dockerTag=${{needs.tag.outputs.build-tag}}
|
||||||
elif [[ "$GITHUB_REF_NAME" == "release-proxy" ]]; then
|
elif [[ "$GITHUB_REF_NAME" == "release-proxy" ]]; then
|
||||||
@@ -1144,6 +1269,7 @@ jobs:
|
|||||||
-f deployProxy=true \
|
-f deployProxy=true \
|
||||||
-f deployStorage=false \
|
-f deployStorage=false \
|
||||||
-f deployStorageBroker=false \
|
-f deployStorageBroker=false \
|
||||||
|
-f deployStorageController=false \
|
||||||
-f branch=main \
|
-f branch=main \
|
||||||
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
-f dockerTag=${{needs.tag.outputs.build-tag}} \
|
||||||
-f deployPreprodRegion=true
|
-f deployPreprodRegion=true
|
||||||
@@ -1214,7 +1340,7 @@ jobs:
|
|||||||
# Update Neon artifact for the release (reuse already uploaded artifact)
|
# Update Neon artifact for the release (reuse already uploaded artifact)
|
||||||
for build_type in debug release; do
|
for build_type in debug release; do
|
||||||
OLD_PREFIX=artifacts/${GITHUB_RUN_ID}
|
OLD_PREFIX=artifacts/${GITHUB_RUN_ID}
|
||||||
FILENAME=neon-${{ runner.os }}-${build_type}-artifact.tar.zst
|
FILENAME=neon-${{ runner.os }}-${{ runner.arch }}-${build_type}-artifact.tar.zst
|
||||||
|
|
||||||
S3_KEY=$(aws s3api list-objects-v2 --bucket ${BUCKET} --prefix ${OLD_PREFIX} | jq -r '.Contents[]?.Key' | grep ${FILENAME} | sort --version-sort | tail -1 || true)
|
S3_KEY=$(aws s3api list-objects-v2 --bucket ${BUCKET} --prefix ${OLD_PREFIX} | jq -r '.Contents[]?.Key' | grep ${FILENAME} | sort --version-sort | tail -1 || true)
|
||||||
if [ -z "${S3_KEY}" ]; then
|
if [ -z "${S3_KEY}" ]; then
|
||||||
|
|||||||
23
.github/workflows/check-build-tools-image.yml
vendored
23
.github/workflows/check-build-tools-image.yml
vendored
@@ -19,30 +19,23 @@ permissions: {}
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-image:
|
check-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
tag: ${{ steps.get-build-tools-tag.outputs.image-tag }}
|
tag: ${{ steps.get-build-tools-tag.outputs.image-tag }}
|
||||||
found: ${{ steps.check-image.outputs.found }}
|
found: ${{ steps.check-image.outputs.found }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Get build-tools image tag for the current commit
|
- name: Get build-tools image tag for the current commit
|
||||||
id: get-build-tools-tag
|
id: get-build-tools-tag
|
||||||
env:
|
env:
|
||||||
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
IMAGE_TAG: |
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
${{ hashFiles('Dockerfile.build-tools',
|
||||||
|
'.github/workflows/check-build-tools-image.yml',
|
||||||
|
'.github/workflows/build-build-tools-image.yml') }}
|
||||||
run: |
|
run: |
|
||||||
LAST_BUILD_TOOLS_SHA=$(
|
echo "image-tag=${IMAGE_TAG}" | tee -a $GITHUB_OUTPUT
|
||||||
gh api \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
--method GET \
|
|
||||||
--field path=Dockerfile.build-tools \
|
|
||||||
--field sha=${COMMIT_SHA} \
|
|
||||||
--field per_page=1 \
|
|
||||||
--jq ".[0].sha" \
|
|
||||||
"/repos/${GITHUB_REPOSITORY}/commits"
|
|
||||||
)
|
|
||||||
echo "image-tag=${LAST_BUILD_TOOLS_SHA}" | tee -a $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Check if such tag found in the registry
|
- name: Check if such tag found in the registry
|
||||||
id: check-image
|
id: check-image
|
||||||
|
|||||||
2
.github/workflows/check-permissions.yml
vendored
2
.github/workflows/check-permissions.yml
vendored
@@ -16,7 +16,7 @@ permissions: {}
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-permissions:
|
check-permissions:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Disallow CI runs on PRs from forks
|
- name: Disallow CI runs on PRs from forks
|
||||||
if: |
|
if: |
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Cleanup
|
- name: Cleanup
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
33
.github/workflows/neon_extra_builds.yml
vendored
33
.github/workflows/neon_extra_builds.yml
vendored
@@ -136,7 +136,7 @@ jobs:
|
|||||||
check-linux-arm-build:
|
check-linux-arm-build:
|
||||||
needs: [ check-permissions, build-build-tools-image ]
|
needs: [ check-permissions, build-build-tools-image ]
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
runs-on: [ self-hosted, dev, arm64 ]
|
runs-on: [ self-hosted, small-arm64 ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
# Use release build only, to have less debug info around
|
# Use release build only, to have less debug info around
|
||||||
@@ -232,20 +232,20 @@ jobs:
|
|||||||
|
|
||||||
- name: Run cargo build
|
- name: Run cargo build
|
||||||
run: |
|
run: |
|
||||||
mold -run cargo build --locked $CARGO_FLAGS $CARGO_FEATURES --bins --tests
|
mold -run cargo build --locked $CARGO_FLAGS $CARGO_FEATURES --bins --tests -j$(nproc)
|
||||||
|
|
||||||
- name: Run cargo test
|
- name: Run cargo test
|
||||||
env:
|
env:
|
||||||
NEXTEST_RETRIES: 3
|
NEXTEST_RETRIES: 3
|
||||||
run: |
|
run: |
|
||||||
cargo nextest run $CARGO_FEATURES
|
cargo nextest run $CARGO_FEATURES -j$(nproc)
|
||||||
|
|
||||||
# Run separate tests for real S3
|
# Run separate tests for real S3
|
||||||
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
export ENABLE_REAL_S3_REMOTE_STORAGE=nonempty
|
||||||
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
export REMOTE_STORAGE_S3_BUCKET=neon-github-ci-tests
|
||||||
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
export REMOTE_STORAGE_S3_REGION=eu-central-1
|
||||||
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
||||||
cargo nextest run --package remote_storage --test test_real_s3
|
cargo nextest run --package remote_storage --test test_real_s3 -j$(nproc)
|
||||||
|
|
||||||
# Run separate tests for real Azure Blob Storage
|
# Run separate tests for real Azure Blob Storage
|
||||||
# XXX: replace region with `eu-central-1`-like region
|
# XXX: replace region with `eu-central-1`-like region
|
||||||
@@ -255,12 +255,12 @@ jobs:
|
|||||||
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
export REMOTE_STORAGE_AZURE_CONTAINER="${{ vars.REMOTE_STORAGE_AZURE_CONTAINER }}"
|
||||||
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
export REMOTE_STORAGE_AZURE_REGION="${{ vars.REMOTE_STORAGE_AZURE_REGION }}"
|
||||||
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
# Avoid `$CARGO_FEATURES` since there's no `testing` feature in the e2e tests now
|
||||||
cargo nextest run --package remote_storage --test test_real_azure
|
cargo nextest run --package remote_storage --test test_real_azure -j$(nproc)
|
||||||
|
|
||||||
check-codestyle-rust-arm:
|
check-codestyle-rust-arm:
|
||||||
needs: [ check-permissions, build-build-tools-image ]
|
needs: [ check-permissions, build-build-tools-image ]
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
runs-on: [ self-hosted, dev, arm64 ]
|
runs-on: [ self-hosted, small-arm64 ]
|
||||||
|
|
||||||
container:
|
container:
|
||||||
image: ${{ needs.build-build-tools-image.outputs.image }}
|
image: ${{ needs.build-build-tools-image.outputs.image }}
|
||||||
@@ -269,6 +269,11 @@ jobs:
|
|||||||
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
password: ${{ secrets.NEON_DOCKERHUB_PASSWORD }}
|
||||||
options: --init
|
options: --init
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
build_type: [ debug, release ]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Fix git ownership
|
- name: Fix git ownership
|
||||||
run: |
|
run: |
|
||||||
@@ -305,31 +310,35 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo "CLIPPY_COMMON_ARGS=${CLIPPY_COMMON_ARGS}" >> $GITHUB_ENV
|
echo "CLIPPY_COMMON_ARGS=${CLIPPY_COMMON_ARGS}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Run cargo clippy (debug)
|
- name: Run cargo clippy (debug)
|
||||||
|
if: matrix.build_type == 'debug'
|
||||||
run: cargo hack --feature-powerset clippy $CLIPPY_COMMON_ARGS
|
run: cargo hack --feature-powerset clippy $CLIPPY_COMMON_ARGS
|
||||||
- name: Run cargo clippy (release)
|
- name: Run cargo clippy (release)
|
||||||
|
if: matrix.build_type == 'release'
|
||||||
run: cargo hack --feature-powerset clippy --release $CLIPPY_COMMON_ARGS
|
run: cargo hack --feature-powerset clippy --release $CLIPPY_COMMON_ARGS
|
||||||
|
|
||||||
- name: Check documentation generation
|
- name: Check documentation generation
|
||||||
run: cargo doc --workspace --no-deps --document-private-items
|
if: matrix.build_type == 'release'
|
||||||
|
run: cargo doc --workspace --no-deps --document-private-items -j$(nproc)
|
||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: "-Dwarnings -Arustdoc::private_intra_doc_links"
|
RUSTDOCFLAGS: "-Dwarnings -Arustdoc::private_intra_doc_links"
|
||||||
|
|
||||||
# Use `${{ !cancelled() }}` to run quck tests after the longer clippy run
|
# Use `${{ !cancelled() }}` to run quck tests after the longer clippy run
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() && matrix.build_type == 'release' }}
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
# https://github.com/facebookincubator/cargo-guppy/tree/bec4e0eb29dcd1faac70b1b5360267fc02bf830e/tools/cargo-hakari#2-keep-the-workspace-hack-up-to-date-in-ci
|
# https://github.com/facebookincubator/cargo-guppy/tree/bec4e0eb29dcd1faac70b1b5360267fc02bf830e/tools/cargo-hakari#2-keep-the-workspace-hack-up-to-date-in-ci
|
||||||
- name: Check rust dependencies
|
- name: Check rust dependencies
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() && matrix.build_type == 'release' }}
|
||||||
run: |
|
run: |
|
||||||
cargo hakari generate --diff # workspace-hack Cargo.toml is up-to-date
|
cargo hakari generate --diff # workspace-hack Cargo.toml is up-to-date
|
||||||
cargo hakari manage-deps --dry-run # all workspace crates depend on workspace-hack
|
cargo hakari manage-deps --dry-run # all workspace crates depend on workspace-hack
|
||||||
|
|
||||||
# https://github.com/EmbarkStudios/cargo-deny
|
# https://github.com/EmbarkStudios/cargo-deny
|
||||||
- name: Check rust licenses/bans/advisories/sources
|
- name: Check rust licenses/bans/advisories/sources
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() && matrix.build_type == 'release' }}
|
||||||
run: cargo deny check
|
run: cargo deny check
|
||||||
|
|
||||||
gather-rust-build-stats:
|
gather-rust-build-stats:
|
||||||
@@ -338,7 +347,7 @@ jobs:
|
|||||||
contains(github.event.pull_request.labels.*.name, 'run-extra-build-stats') ||
|
contains(github.event.pull_request.labels.*.name, 'run-extra-build-stats') ||
|
||||||
contains(github.event.pull_request.labels.*.name, 'run-extra-build-*') ||
|
contains(github.event.pull_request.labels.*.name, 'run-extra-build-*') ||
|
||||||
github.ref_name == 'main'
|
github.ref_name == 'main'
|
||||||
runs-on: [ self-hosted, gen3, large ]
|
runs-on: [ self-hosted, large ]
|
||||||
container:
|
container:
|
||||||
image: ${{ needs.build-build-tools-image.outputs.image }}
|
image: ${{ needs.build-build-tools-image.outputs.image }}
|
||||||
credentials:
|
credentials:
|
||||||
@@ -369,7 +378,7 @@ jobs:
|
|||||||
run: make walproposer-lib -j$(nproc)
|
run: make walproposer-lib -j$(nproc)
|
||||||
|
|
||||||
- name: Produce the build stats
|
- name: Produce the build stats
|
||||||
run: cargo build --all --release --timings
|
run: cargo build --all --release --timings -j$(nproc)
|
||||||
|
|
||||||
- name: Upload the build stats
|
- name: Upload the build stats
|
||||||
id: upload-stats
|
id: upload-stats
|
||||||
|
|||||||
6
.github/workflows/pg_clients.yml
vendored
6
.github/workflows/pg_clients.yml
vendored
@@ -20,7 +20,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
test-postgres-client-libs:
|
test-postgres-client-libs:
|
||||||
# TODO: switch to gen2 runner, requires docker
|
# TODO: switch to gen2 runner, requires docker
|
||||||
runs-on: [ ubuntu-latest ]
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DEFAULT_PG_VERSION: 14
|
DEFAULT_PG_VERSION: 14
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ~/.cache/pypoetry/virtualenvs
|
path: ~/.cache/pypoetry/virtualenvs
|
||||||
key: v2-${{ runner.os }}-python-deps-ubunutu-latest-${{ hashFiles('poetry.lock') }}
|
key: v2-${{ runner.os }}-${{ runner.arch }}-python-deps-ubunutu-latest-${{ hashFiles('poetry.lock') }}
|
||||||
|
|
||||||
- name: Install Python deps
|
- name: Install Python deps
|
||||||
shell: bash -euxo pipefail {0}
|
shell: bash -euxo pipefail {0}
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
name: python-test-pg_clients-${{ runner.os }}-stage-logs
|
name: python-test-pg_clients-${{ runner.os }}-${{ runner.arch }}-stage-logs
|
||||||
path: ${{ env.TEST_OUTPUT }}
|
path: ${{ env.TEST_OUTPUT }}
|
||||||
|
|
||||||
- name: Post to a Slack channel
|
- name: Post to a Slack channel
|
||||||
|
|||||||
3
.github/workflows/pin-build-tools-image.yml
vendored
3
.github/workflows/pin-build-tools-image.yml
vendored
@@ -20,12 +20,13 @@ defaults:
|
|||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: pin-build-tools-image-${{ inputs.from-tag }}
|
group: pin-build-tools-image-${{ inputs.from-tag }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
permissions: {}
|
permissions: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tag-image:
|
tag-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
env:
|
env:
|
||||||
FROM_TAG: ${{ inputs.from-tag }}
|
FROM_TAG: ${{ inputs.from-tag }}
|
||||||
|
|||||||
2
.github/workflows/release-notify.yml
vendored
2
.github/workflows/release-notify.yml
vendored
@@ -19,7 +19,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
notify:
|
notify:
|
||||||
runs-on: [ ubuntu-latest ]
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: neondatabase/dev-actions/release-pr-notify@main
|
- uses: neondatabase/dev-actions/release-pr-notify@main
|
||||||
|
|||||||
16
.github/workflows/release.yml
vendored
16
.github/workflows/release.yml
vendored
@@ -26,7 +26,7 @@ defaults:
|
|||||||
jobs:
|
jobs:
|
||||||
create-storage-release-branch:
|
create-storage-release-branch:
|
||||||
if: ${{ github.event.schedule == '0 6 * * MON' || format('{0}', inputs.create-storage-release-branch) == 'true' }}
|
if: ${{ github.event.schedule == '0 6 * * MON' || format('{0}', inputs.create-storage-release-branch) == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # for `git push`
|
contents: write # for `git push`
|
||||||
@@ -52,20 +52,22 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
TITLE="Storage & Compute release ${RELEASE_DATE}"
|
||||||
|
|
||||||
cat << EOF > body.md
|
cat << EOF > body.md
|
||||||
## Release ${RELEASE_DATE}
|
## ${TITLE}
|
||||||
|
|
||||||
**Please merge this Pull Request using 'Create a merge commit' button**
|
**Please merge this Pull Request using 'Create a merge commit' button**
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
gh pr create --title "Release ${RELEASE_DATE}" \
|
gh pr create --title "${TITLE}" \
|
||||||
--body-file "body.md" \
|
--body-file "body.md" \
|
||||||
--head "${RELEASE_BRANCH}" \
|
--head "${RELEASE_BRANCH}" \
|
||||||
--base "release"
|
--base "release"
|
||||||
|
|
||||||
create-proxy-release-branch:
|
create-proxy-release-branch:
|
||||||
if: ${{ github.event.schedule == '0 6 * * THU' || format('{0}', inputs.create-proxy-release-branch) == 'true' }}
|
if: ${{ github.event.schedule == '0 6 * * THU' || format('{0}', inputs.create-proxy-release-branch) == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write # for `git push`
|
contents: write # for `git push`
|
||||||
@@ -91,13 +93,15 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
TITLE="Proxy release ${RELEASE_DATE}"
|
||||||
|
|
||||||
cat << EOF > body.md
|
cat << EOF > body.md
|
||||||
## Proxy release ${RELEASE_DATE}
|
## ${TITLE}
|
||||||
|
|
||||||
**Please merge this Pull Request using 'Create a merge commit' button**
|
**Please merge this Pull Request using 'Create a merge commit' button**
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
gh pr create --title "Proxy release ${RELEASE_DATE}" \
|
gh pr create --title "${TITLE}" \
|
||||||
--body-file "body.md" \
|
--body-file "body.md" \
|
||||||
--head "${RELEASE_BRANCH}" \
|
--head "${RELEASE_BRANCH}" \
|
||||||
--base "release-proxy"
|
--base "release-proxy"
|
||||||
|
|||||||
94
.github/workflows/trigger-e2e-tests.yml
vendored
94
.github/workflows/trigger-e2e-tests.yml
vendored
@@ -19,7 +19,7 @@ env:
|
|||||||
jobs:
|
jobs:
|
||||||
cancel-previous-e2e-tests:
|
cancel-previous-e2e-tests:
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Cancel previous e2e-tests runs for this PR
|
- name: Cancel previous e2e-tests runs for this PR
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
--field concurrency_group="${{ env.E2E_CONCURRENCY_GROUP }}"
|
--field concurrency_group="${{ env.E2E_CONCURRENCY_GROUP }}"
|
||||||
|
|
||||||
tag:
|
tag:
|
||||||
runs-on: [ ubuntu-latest ]
|
runs-on: ubuntu-22.04
|
||||||
outputs:
|
outputs:
|
||||||
build-tag: ${{ steps.build-tag.outputs.tag }}
|
build-tag: ${{ steps.build-tag.outputs.tag }}
|
||||||
|
|
||||||
@@ -62,14 +62,14 @@ jobs:
|
|||||||
|
|
||||||
trigger-e2e-tests:
|
trigger-e2e-tests:
|
||||||
needs: [ tag ]
|
needs: [ tag ]
|
||||||
runs-on: [ self-hosted, gen3, small ]
|
runs-on: ubuntu-22.04
|
||||||
env:
|
env:
|
||||||
TAG: ${{ needs.tag.outputs.build-tag }}
|
TAG: ${{ needs.tag.outputs.build-tag }}
|
||||||
container:
|
|
||||||
image: 369495373322.dkr.ecr.eu-central-1.amazonaws.com/base:pinned
|
|
||||||
options: --init
|
|
||||||
steps:
|
steps:
|
||||||
- name: check if ecr image are present
|
- name: check if ecr image are present
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_DEV }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_KEY_DEV }}
|
||||||
run: |
|
run: |
|
||||||
for REPO in neon compute-tools compute-node-v14 vm-compute-node-v14 compute-node-v15 vm-compute-node-v15 compute-node-v16 vm-compute-node-v16; do
|
for REPO in neon compute-tools compute-node-v14 vm-compute-node-v14 compute-node-v15 vm-compute-node-v15 compute-node-v16 vm-compute-node-v16; do
|
||||||
OUTPUT=$(aws ecr describe-images --repository-name ${REPO} --region eu-central-1 --query "imageDetails[?imageTags[?contains(@, '${TAG}')]]" --output text)
|
OUTPUT=$(aws ecr describe-images --repository-name ${REPO} --region eu-central-1 --query "imageDetails[?imageTags[?contains(@, '${TAG}')]]" --output text)
|
||||||
@@ -79,41 +79,55 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
- name: Set PR's status to pending and request a remote CI test
|
- name: Set e2e-platforms
|
||||||
|
id: e2e-platforms
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
# For pull requests, GH Actions set "github.sha" variable to point at a fake merge commit
|
# Default set of platforms to run e2e tests on
|
||||||
# but we need to use a real sha of a latest commit in the PR's branch for the e2e job,
|
platforms='["docker", "k8s"]'
|
||||||
# to place a job run status update later.
|
|
||||||
COMMIT_SHA=${{ github.event.pull_request.head.sha }}
|
|
||||||
# For non-PR kinds of runs, the above will produce an empty variable, pick the original sha value for those
|
|
||||||
COMMIT_SHA=${COMMIT_SHA:-${{ github.sha }}}
|
|
||||||
|
|
||||||
REMOTE_REPO="${{ github.repository_owner }}/cloud"
|
# If the PR changes vendor/, pgxn/ or libs/vm_monitor/ directories, or Dockerfile.compute-node, add k8s-neonvm to the list of platforms.
|
||||||
|
# If the workflow run is not a pull request, add k8s-neonvm to the list.
|
||||||
|
if [ "$GITHUB_EVENT_NAME" == "pull_request" ]; then
|
||||||
|
for f in $(gh api "/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'); do
|
||||||
|
case "$f" in
|
||||||
|
vendor/*|pgxn/*|libs/vm_monitor/*|Dockerfile.compute-node)
|
||||||
|
platforms=$(echo "${platforms}" | jq --compact-output '. += ["k8s-neonvm"] | unique')
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# no-op
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
else
|
||||||
|
platforms=$(echo "${platforms}" | jq --compact-output '. += ["k8s-neonvm"] | unique')
|
||||||
|
fi
|
||||||
|
|
||||||
curl -f -X POST \
|
echo "e2e-platforms=${platforms}" | tee -a $GITHUB_OUTPUT
|
||||||
https://api.github.com/repos/${{ github.repository }}/statuses/$COMMIT_SHA \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
--user "${{ secrets.CI_ACCESS_TOKEN }}" \
|
|
||||||
--data \
|
|
||||||
"{
|
|
||||||
\"state\": \"pending\",
|
|
||||||
\"context\": \"neon-cloud-e2e\",
|
|
||||||
\"description\": \"[$REMOTE_REPO] Remote CI job is about to start\"
|
|
||||||
}"
|
|
||||||
|
|
||||||
curl -f -X POST \
|
- name: Set PR's status to pending and request a remote CI test
|
||||||
https://api.github.com/repos/$REMOTE_REPO/actions/workflows/testing.yml/dispatches \
|
env:
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
E2E_PLATFORMS: ${{ steps.e2e-platforms.outputs.e2e-platforms }}
|
||||||
--user "${{ secrets.CI_ACCESS_TOKEN }}" \
|
COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
--data \
|
GH_TOKEN: ${{ secrets.CI_ACCESS_TOKEN }}
|
||||||
"{
|
run: |
|
||||||
\"ref\": \"main\",
|
REMOTE_REPO="${GITHUB_REPOSITORY_OWNER}/cloud"
|
||||||
\"inputs\": {
|
|
||||||
\"ci_job_name\": \"neon-cloud-e2e\",
|
gh api "/repos/${GITHUB_REPOSITORY}/statuses/${COMMIT_SHA}" \
|
||||||
\"commit_hash\": \"$COMMIT_SHA\",
|
--method POST \
|
||||||
\"remote_repo\": \"${{ github.repository }}\",
|
--raw-field "state=pending" \
|
||||||
\"storage_image_tag\": \"${TAG}\",
|
--raw-field "description=[$REMOTE_REPO] Remote CI job is about to start" \
|
||||||
\"compute_image_tag\": \"${TAG}\",
|
--raw-field "context=neon-cloud-e2e"
|
||||||
\"concurrency_group\": \"${{ env.E2E_CONCURRENCY_GROUP }}\"
|
|
||||||
}
|
gh workflow --repo ${REMOTE_REPO} \
|
||||||
}"
|
run testing.yml \
|
||||||
|
--ref "main" \
|
||||||
|
--raw-field "ci_job_name=neon-cloud-e2e" \
|
||||||
|
--raw-field "commit_hash=$COMMIT_SHA" \
|
||||||
|
--raw-field "remote_repo=${GITHUB_REPOSITORY}" \
|
||||||
|
--raw-field "storage_image_tag=${TAG}" \
|
||||||
|
--raw-field "compute_image_tag=${TAG}" \
|
||||||
|
--raw-field "concurrency_group=${E2E_CONCURRENCY_GROUP}" \
|
||||||
|
--raw-field "e2e-platforms=${E2E_PLATFORMS}"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# * `-A unknown_lints` – do not warn about unknown lint suppressions
|
# * `-A unknown_lints` – do not warn about unknown lint suppressions
|
||||||
# that people with newer toolchains might use
|
# that people with newer toolchains might use
|
||||||
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status)
|
# * `-D warnings` - fail on any warnings (`cargo` returns non-zero exit status)
|
||||||
export CLIPPY_COMMON_ARGS="--locked --workspace --all-targets -- -A unknown_lints -D warnings"
|
# * `-D clippy::todo` - don't let `todo!()` slip into `main`
|
||||||
|
export CLIPPY_COMMON_ARGS="--locked --workspace --all-targets -- -A unknown_lints -D warnings -D clippy::todo"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/compute_tools/ @neondatabase/control-plane @neondatabase/compute
|
/compute_tools/ @neondatabase/control-plane @neondatabase/compute
|
||||||
/control_plane/attachment_service @neondatabase/storage
|
/storage_controller @neondatabase/storage
|
||||||
/libs/pageserver_api/ @neondatabase/storage
|
/libs/pageserver_api/ @neondatabase/storage
|
||||||
/libs/postgres_ffi/ @neondatabase/compute @neondatabase/safekeepers
|
/libs/postgres_ffi/ @neondatabase/compute @neondatabase/safekeepers
|
||||||
/libs/remote_storage/ @neondatabase/storage
|
/libs/remote_storage/ @neondatabase/storage
|
||||||
|
|||||||
1403
Cargo.lock
generated
1403
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
80
Cargo.toml
80
Cargo.toml
@@ -3,7 +3,7 @@ resolver = "2"
|
|||||||
members = [
|
members = [
|
||||||
"compute_tools",
|
"compute_tools",
|
||||||
"control_plane",
|
"control_plane",
|
||||||
"control_plane/attachment_service",
|
"control_plane/storcon_cli",
|
||||||
"pageserver",
|
"pageserver",
|
||||||
"pageserver/compaction",
|
"pageserver/compaction",
|
||||||
"pageserver/ctl",
|
"pageserver/ctl",
|
||||||
@@ -12,7 +12,8 @@ members = [
|
|||||||
"proxy",
|
"proxy",
|
||||||
"safekeeper",
|
"safekeeper",
|
||||||
"storage_broker",
|
"storage_broker",
|
||||||
"s3_scrubber",
|
"storage_controller",
|
||||||
|
"storage_scrubber",
|
||||||
"workspace_hack",
|
"workspace_hack",
|
||||||
"trace",
|
"trace",
|
||||||
"libs/compute_api",
|
"libs/compute_api",
|
||||||
@@ -40,24 +41,26 @@ license = "Apache-2.0"
|
|||||||
|
|
||||||
## All dependency versions, used in the project
|
## All dependency versions, used in the project
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
ahash = "0.8"
|
||||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||||
arc-swap = "1.6"
|
arc-swap = "1.6"
|
||||||
async-compression = { version = "0.4.0", features = ["tokio", "gzip", "zstd"] }
|
async-compression = { version = "0.4.0", features = ["tokio", "gzip", "zstd"] }
|
||||||
azure_core = "0.18"
|
atomic-take = "1.1.0"
|
||||||
azure_identity = "0.18"
|
azure_core = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls", "hmac_rust"] }
|
||||||
azure_storage = "0.18"
|
azure_identity = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls"] }
|
||||||
azure_storage_blobs = "0.18"
|
azure_storage = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls"] }
|
||||||
|
azure_storage_blobs = { version = "0.19", default-features = false, features = ["enable_reqwest_rustls"] }
|
||||||
flate2 = "1.0.26"
|
flate2 = "1.0.26"
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
aws-config = { version = "1.1.4", default-features = false, features=["rustls"] }
|
aws-config = { version = "1.3", default-features = false, features=["rustls"] }
|
||||||
aws-sdk-s3 = "1.14"
|
aws-sdk-s3 = "1.26"
|
||||||
aws-sdk-iam = "1.15.0"
|
aws-sdk-iam = "1.15.0"
|
||||||
aws-smithy-async = { version = "1.1.4", default-features = false, features=["rt-tokio"] }
|
aws-smithy-async = { version = "1.2.1", default-features = false, features=["rt-tokio"] }
|
||||||
aws-smithy-types = "1.1.4"
|
aws-smithy-types = "1.1.9"
|
||||||
aws-credential-types = "1.1.4"
|
aws-credential-types = "1.2.0"
|
||||||
aws-sigv4 = { version = "1.2.0", features = ["sign-http"] }
|
aws-sigv4 = { version = "1.2.1", features = ["sign-http"] }
|
||||||
aws-types = "1.1.7"
|
aws-types = "1.2.0"
|
||||||
axum = { version = "0.6.20", features = ["ws"] }
|
axum = { version = "0.6.20", features = ["ws"] }
|
||||||
base64 = "0.13.0"
|
base64 = "0.13.0"
|
||||||
bincode = "1.3"
|
bincode = "1.3"
|
||||||
@@ -72,6 +75,7 @@ clap = { version = "4.0", features = ["derive"] }
|
|||||||
comfy-table = "6.1"
|
comfy-table = "6.1"
|
||||||
const_format = "0.2"
|
const_format = "0.2"
|
||||||
crc32c = "0.6"
|
crc32c = "0.6"
|
||||||
|
crossbeam-deque = "0.8.5"
|
||||||
crossbeam-utils = "0.8.5"
|
crossbeam-utils = "0.8.5"
|
||||||
dashmap = { version = "5.5.0", features = ["raw-api"] }
|
dashmap = { version = "5.5.0", features = ["raw-api"] }
|
||||||
either = "1.8"
|
either = "1.8"
|
||||||
@@ -79,13 +83,14 @@ enum-map = "2.4.2"
|
|||||||
enumset = "1.0.12"
|
enumset = "1.0.12"
|
||||||
fail = "0.5.0"
|
fail = "0.5.0"
|
||||||
fallible-iterator = "0.2"
|
fallible-iterator = "0.2"
|
||||||
|
framed-websockets = { version = "0.1.0", git = "https://github.com/neondatabase/framed-websockets" }
|
||||||
fs2 = "0.4.3"
|
fs2 = "0.4.3"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
git-version = "0.3"
|
git-version = "0.3"
|
||||||
hashbrown = "0.13"
|
hashbrown = "0.14"
|
||||||
hashlink = "0.8.4"
|
hashlink = "0.9.1"
|
||||||
hdrhistogram = "7.5.2"
|
hdrhistogram = "7.5.2"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
hex-literal = "0.4"
|
hex-literal = "0.4"
|
||||||
@@ -96,7 +101,8 @@ http-types = { version = "2", default-features = false }
|
|||||||
humantime = "2.1"
|
humantime = "2.1"
|
||||||
humantime-serde = "1.1.1"
|
humantime-serde = "1.1.1"
|
||||||
hyper = "0.14"
|
hyper = "0.14"
|
||||||
hyper-tungstenite = "0.11"
|
tokio-tungstenite = "0.20.0"
|
||||||
|
indexmap = "2"
|
||||||
inotify = "0.10.2"
|
inotify = "0.10.2"
|
||||||
ipnet = "2.9.0"
|
ipnet = "2.9.0"
|
||||||
itertools = "0.10"
|
itertools = "0.10"
|
||||||
@@ -105,32 +111,32 @@ lasso = "0.7"
|
|||||||
leaky-bucket = "1.0.1"
|
leaky-bucket = "1.0.1"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
md5 = "0.7.0"
|
md5 = "0.7.0"
|
||||||
measured = { version = "0.0.13", features=["default", "lasso"] }
|
measured = { version = "0.0.21", features=["lasso"] }
|
||||||
|
measured-process = { version = "0.0.21" }
|
||||||
memoffset = "0.8"
|
memoffset = "0.8"
|
||||||
native-tls = "0.2"
|
|
||||||
nix = { version = "0.27", features = ["fs", "process", "socket", "signal", "poll"] }
|
nix = { version = "0.27", features = ["fs", "process", "socket", "signal", "poll"] }
|
||||||
notify = "6.0.0"
|
notify = "6.0.0"
|
||||||
num_cpus = "1.15"
|
num_cpus = "1.15"
|
||||||
num-traits = "0.2.15"
|
num-traits = "0.2.15"
|
||||||
once_cell = "1.13"
|
once_cell = "1.13"
|
||||||
opentelemetry = "0.20.0"
|
opentelemetry = "0.20.0"
|
||||||
opentelemetry-otlp = { version = "0.13.0", default_features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
opentelemetry-otlp = { version = "0.13.0", default-features=false, features = ["http-proto", "trace", "http", "reqwest-client"] }
|
||||||
opentelemetry-semantic-conventions = "0.12.0"
|
opentelemetry-semantic-conventions = "0.12.0"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
parquet = { version = "49.0.0", default-features = false, features = ["zstd"] }
|
parquet = { version = "51.0.0", default-features = false, features = ["zstd"] }
|
||||||
parquet_derive = "49.0.0"
|
parquet_derive = "51.0.0"
|
||||||
pbkdf2 = { version = "0.12.1", features = ["simple", "std"] }
|
pbkdf2 = { version = "0.12.1", features = ["simple", "std"] }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
procfs = "0.14"
|
procfs = "0.14"
|
||||||
prometheus = {version = "0.13", default_features=false, features = ["process"]} # removes protobuf dependency
|
prometheus = {version = "0.13", default-features=false, features = ["process"]} # removes protobuf dependency
|
||||||
prost = "0.11"
|
prost = "0.11"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
redis = { version = "0.25.2", features = ["tokio-rustls-comp", "keep-alive"] }
|
redis = { version = "0.25.2", features = ["tokio-rustls-comp", "keep-alive"] }
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] }
|
||||||
reqwest-tracing = { version = "0.4.7", features = ["opentelemetry_0_20"] }
|
reqwest-tracing = { version = "0.5", features = ["opentelemetry_0_20"] }
|
||||||
reqwest-middleware = "0.2.0"
|
reqwest-middleware = "0.3.0"
|
||||||
reqwest-retry = "0.2.2"
|
reqwest-retry = "0.5"
|
||||||
routerify = "3"
|
routerify = "3"
|
||||||
rpds = "0.13"
|
rpds = "0.13"
|
||||||
rustc-hash = "1.1.0"
|
rustc-hash = "1.1.0"
|
||||||
@@ -140,7 +146,7 @@ rustls-split = "0.3"
|
|||||||
scopeguard = "1.1"
|
scopeguard = "1.1"
|
||||||
sysinfo = "0.29.2"
|
sysinfo = "0.29.2"
|
||||||
sd-notify = "0.4.1"
|
sd-notify = "0.4.1"
|
||||||
sentry = { version = "0.31", default-features = false, features = ["backtrace", "contexts", "panic", "rustls", "reqwest" ] }
|
sentry = { version = "0.32", default-features = false, features = ["backtrace", "contexts", "panic", "rustls", "reqwest" ] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_path_to_error = "0.1"
|
serde_path_to_error = "0.1"
|
||||||
@@ -154,11 +160,12 @@ socket2 = "0.5"
|
|||||||
strum = "0.24"
|
strum = "0.24"
|
||||||
strum_macros = "0.24"
|
strum_macros = "0.24"
|
||||||
"subtle" = "2.5.0"
|
"subtle" = "2.5.0"
|
||||||
svg_fmt = "0.4.1"
|
# Our PR https://github.com/nical/rust_debug/pull/4 has been merged but no new version released yet
|
||||||
|
svg_fmt = { git = "https://github.com/nical/rust_debug", rev = "28a7d96eecff2f28e75b1ea09f2d499a60d0e3b4" }
|
||||||
sync_wrapper = "0.1.2"
|
sync_wrapper = "0.1.2"
|
||||||
tar = "0.4"
|
tar = "0.4"
|
||||||
task-local-extensions = "0.1.4"
|
task-local-extensions = "0.1.4"
|
||||||
test-context = "0.1"
|
test-context = "0.3"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tikv-jemallocator = "0.5"
|
tikv-jemallocator = "0.5"
|
||||||
tikv-jemalloc-ctl = "0.5"
|
tikv-jemalloc-ctl = "0.5"
|
||||||
@@ -173,16 +180,17 @@ tokio-util = { version = "0.7.10", features = ["io", "rt"] }
|
|||||||
toml = "0.7"
|
toml = "0.7"
|
||||||
toml_edit = "0.19"
|
toml_edit = "0.19"
|
||||||
tonic = {version = "0.9", features = ["tls", "tls-roots"]}
|
tonic = {version = "0.9", features = ["tls", "tls-roots"]}
|
||||||
|
tower-service = "0.3.2"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-error = "0.2.0"
|
tracing-error = "0.2.0"
|
||||||
tracing-opentelemetry = "0.20.0"
|
tracing-opentelemetry = "0.21.0"
|
||||||
tracing-subscriber = { version = "0.3", default_features = false, features = ["smallvec", "fmt", "tracing-log", "std", "env-filter", "json"] }
|
tracing-subscriber = { version = "0.3", default-features = false, features = ["smallvec", "fmt", "tracing-log", "std", "env-filter", "json", "ansi"] }
|
||||||
twox-hash = { version = "1.6.3", default-features = false }
|
twox-hash = { version = "1.6.3", default-features = false }
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
urlencoding = "2.1"
|
urlencoding = "2.1"
|
||||||
uuid = { version = "1.6.1", features = ["v4", "v7", "serde"] }
|
uuid = { version = "1.6.1", features = ["v4", "v7", "serde"] }
|
||||||
walkdir = "2.3.2"
|
walkdir = "2.3.2"
|
||||||
webpki-roots = "0.25"
|
rustls-native-certs = "0.7"
|
||||||
x509-parser = "0.15"
|
x509-parser = "0.15"
|
||||||
|
|
||||||
## TODO replace this with tracing
|
## TODO replace this with tracing
|
||||||
@@ -191,7 +199,6 @@ log = "0.4"
|
|||||||
|
|
||||||
## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed
|
## Libraries from neondatabase/ git forks, ideally with changes to be upstreamed
|
||||||
postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
postgres-native-tls = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
|
||||||
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
postgres-protocol = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
postgres-types = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
@@ -232,13 +239,12 @@ tonic-build = "0.9"
|
|||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
|
|
||||||
# This is only needed for proxy's tests.
|
# Needed to get `tokio-postgres-rustls` to depend on our fork.
|
||||||
# TODO: we should probably fork `tokio-postgres-rustls` instead.
|
|
||||||
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
tokio-postgres = { git = "https://github.com/neondatabase/rust-postgres.git", branch="neon" }
|
||||||
|
|
||||||
# bug fixes for UUID
|
# bug fixes for UUID
|
||||||
parquet = { git = "https://github.com/neondatabase/arrow-rs", branch = "neon-fix-bugs" }
|
parquet = { git = "https://github.com/apache/arrow-rs", branch = "master" }
|
||||||
parquet_derive = { git = "https://github.com/neondatabase/arrow-rs", branch = "neon-fix-bugs" }
|
parquet_derive = { git = "https://github.com/apache/arrow-rs", branch = "master" }
|
||||||
|
|
||||||
################# Binary contents sections
|
################# Binary contents sections
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ RUN set -e \
|
|||||||
&& apt install -y \
|
&& apt install -y \
|
||||||
libreadline-dev \
|
libreadline-dev \
|
||||||
libseccomp-dev \
|
libseccomp-dev \
|
||||||
libicu67 \
|
|
||||||
openssl \
|
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
|
||||||
&& useradd -d /data neon \
|
&& useradd -d /data neon \
|
||||||
|
|||||||
@@ -58,8 +58,14 @@ RUN curl -fsSL "https://github.com/protocolbuffers/protobuf/releases/download/v$
|
|||||||
&& mv protoc/include/google /usr/local/include/google \
|
&& mv protoc/include/google /usr/local/include/google \
|
||||||
&& rm -rf protoc.zip protoc
|
&& rm -rf protoc.zip protoc
|
||||||
|
|
||||||
|
# s5cmd
|
||||||
|
ENV S5CMD_VERSION=2.2.2
|
||||||
|
RUN curl -sL "https://github.com/peak/s5cmd/releases/download/v${S5CMD_VERSION}/s5cmd_${S5CMD_VERSION}_Linux-$(uname -m | sed 's/x86_64/64bit/g' | sed 's/aarch64/arm64/g').tar.gz" | tar zxvf - s5cmd \
|
||||||
|
&& chmod +x s5cmd \
|
||||||
|
&& mv s5cmd /usr/local/bin/s5cmd
|
||||||
|
|
||||||
# LLVM
|
# LLVM
|
||||||
ENV LLVM_VERSION=17
|
ENV LLVM_VERSION=18
|
||||||
RUN curl -fsSL 'https://apt.llvm.org/llvm-snapshot.gpg.key' | apt-key add - \
|
RUN curl -fsSL 'https://apt.llvm.org/llvm-snapshot.gpg.key' | apt-key add - \
|
||||||
&& echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.stable.list \
|
&& echo "deb http://apt.llvm.org/bullseye/ llvm-toolchain-bullseye-${LLVM_VERSION} main" > /etc/apt/sources.list.d/llvm.stable.list \
|
||||||
&& apt update \
|
&& apt update \
|
||||||
@@ -67,13 +73,6 @@ RUN curl -fsSL 'https://apt.llvm.org/llvm-snapshot.gpg.key' | apt-key add - \
|
|||||||
&& bash -c 'for f in /usr/bin/clang*-${LLVM_VERSION} /usr/bin/llvm*-${LLVM_VERSION}; do ln -s "${f}" "${f%-${LLVM_VERSION}}"; done' \
|
&& bash -c 'for f in /usr/bin/clang*-${LLVM_VERSION} /usr/bin/llvm*-${LLVM_VERSION}; do ln -s "${f}" "${f%-${LLVM_VERSION}}"; done' \
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||||
|
|
||||||
# PostgreSQL 14
|
|
||||||
RUN curl -fsSL 'https://www.postgresql.org/media/keys/ACCC4CF8.asc' | apt-key add - \
|
|
||||||
&& echo 'deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main' > /etc/apt/sources.list.d/pgdg.list \
|
|
||||||
&& apt update \
|
|
||||||
&& apt install -y postgresql-client-14 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
|
|
||||||
# AWS CLI
|
# AWS CLI
|
||||||
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "awscliv2.zip" \
|
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "awscliv2.zip" \
|
||||||
&& unzip -q awscliv2.zip \
|
&& unzip -q awscliv2.zip \
|
||||||
@@ -81,7 +80,7 @@ RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "aws
|
|||||||
&& rm awscliv2.zip
|
&& rm awscliv2.zip
|
||||||
|
|
||||||
# Mold: A Modern Linker
|
# Mold: A Modern Linker
|
||||||
ENV MOLD_VERSION v2.4.0
|
ENV MOLD_VERSION v2.31.0
|
||||||
RUN set -e \
|
RUN set -e \
|
||||||
&& git clone https://github.com/rui314/mold.git \
|
&& git clone https://github.com/rui314/mold.git \
|
||||||
&& mkdir mold/build \
|
&& mkdir mold/build \
|
||||||
@@ -106,6 +105,45 @@ RUN for package in Capture::Tiny DateTime Devel::Cover Digest::MD5 File::Spec JS
|
|||||||
&& make install \
|
&& make install \
|
||||||
&& rm -rf ../lcov.tar.gz
|
&& rm -rf ../lcov.tar.gz
|
||||||
|
|
||||||
|
# Compile and install the static OpenSSL library
|
||||||
|
ENV OPENSSL_VERSION=1.1.1w
|
||||||
|
ENV OPENSSL_PREFIX=/usr/local/openssl
|
||||||
|
RUN wget -O /tmp/openssl-${OPENSSL_VERSION}.tar.gz https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz && \
|
||||||
|
echo "cf3098950cb4d853ad95c0841f1f9c6d3dc102dccfcacd521d93925208b76ac8 /tmp/openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum --check && \
|
||||||
|
cd /tmp && \
|
||||||
|
tar xzvf /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
||||||
|
rm /tmp/openssl-${OPENSSL_VERSION}.tar.gz && \
|
||||||
|
cd /tmp/openssl-${OPENSSL_VERSION} && \
|
||||||
|
./config --prefix=${OPENSSL_PREFIX} -static --static no-shared -fPIC && \
|
||||||
|
make -j "$(nproc)" && \
|
||||||
|
make install && \
|
||||||
|
cd /tmp && \
|
||||||
|
rm -rf /tmp/openssl-${OPENSSL_VERSION}
|
||||||
|
|
||||||
|
# Use the same version of libicu as the compute nodes so that
|
||||||
|
# clusters created using inidb on pageserver can be used by computes.
|
||||||
|
#
|
||||||
|
# TODO: at this time, Dockerfile.compute-node uses the debian bullseye libicu
|
||||||
|
# package, which is 67.1. We're duplicating that knowledge here, and also, technically,
|
||||||
|
# Debian has a few patches on top of 67.1 that we're not adding here.
|
||||||
|
ENV ICU_VERSION=67.1
|
||||||
|
ENV ICU_PREFIX=/usr/local/icu
|
||||||
|
|
||||||
|
# Download and build static ICU
|
||||||
|
RUN wget -O /tmp/libicu-${ICU_VERSION}.tgz https://github.com/unicode-org/icu/releases/download/release-${ICU_VERSION//./-}/icu4c-${ICU_VERSION//./_}-src.tgz && \
|
||||||
|
echo "94a80cd6f251a53bd2a997f6f1b5ac6653fe791dfab66e1eb0227740fb86d5dc /tmp/libicu-${ICU_VERSION}.tgz" | sha256sum --check && \
|
||||||
|
mkdir /tmp/icu && \
|
||||||
|
pushd /tmp/icu && \
|
||||||
|
tar -xzf /tmp/libicu-${ICU_VERSION}.tgz && \
|
||||||
|
pushd icu/source && \
|
||||||
|
./configure --prefix=${ICU_PREFIX} --enable-static --enable-shared=no CXXFLAGS="-fPIC" CFLAGS="-fPIC" && \
|
||||||
|
make -j "$(nproc)" && \
|
||||||
|
make install && \
|
||||||
|
popd && \
|
||||||
|
rm -rf icu && \
|
||||||
|
rm -f /tmp/libicu-${ICU_VERSION}.tgz && \
|
||||||
|
popd
|
||||||
|
|
||||||
# Switch to nonroot user
|
# Switch to nonroot user
|
||||||
USER nonroot:nonroot
|
USER nonroot:nonroot
|
||||||
WORKDIR /home/nonroot
|
WORKDIR /home/nonroot
|
||||||
@@ -135,7 +173,7 @@ WORKDIR /home/nonroot
|
|||||||
|
|
||||||
# Rust
|
# Rust
|
||||||
# Please keep the version of llvm (installed above) in sync with rust llvm (`rustc --version --verbose | grep LLVM`)
|
# Please keep the version of llvm (installed above) in sync with rust llvm (`rustc --version --verbose | grep LLVM`)
|
||||||
ENV RUSTC_VERSION=1.77.0
|
ENV RUSTC_VERSION=1.79.0
|
||||||
ENV RUSTUP_HOME="/home/nonroot/.rustup"
|
ENV RUSTUP_HOME="/home/nonroot/.rustup"
|
||||||
ENV PATH="/home/nonroot/.cargo/bin:${PATH}"
|
ENV PATH="/home/nonroot/.cargo/bin:${PATH}"
|
||||||
RUN curl -sSO https://static.rust-lang.org/rustup/dist/$(uname -m)-unknown-linux-gnu/rustup-init && whoami && \
|
RUN curl -sSO https://static.rust-lang.org/rustup/dist/$(uname -m)-unknown-linux-gnu/rustup-init && whoami && \
|
||||||
@@ -164,3 +202,6 @@ RUN whoami \
|
|||||||
&& rustup --version --verbose \
|
&& rustup --version --verbose \
|
||||||
&& rustc --version --verbose \
|
&& rustc --version --verbose \
|
||||||
&& clang --version
|
&& clang --version
|
||||||
|
|
||||||
|
# Set following flag to check in Makefile if its running in Docker
|
||||||
|
RUN touch /home/nonroot/.docker_build
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ RUN apt update && \
|
|||||||
# SFCGAL > 1.3 requires CGAL > 5.2, Bullseye's libcgal-dev is 5.2
|
# SFCGAL > 1.3 requires CGAL > 5.2, Bullseye's libcgal-dev is 5.2
|
||||||
RUN wget https://gitlab.com/Oslandia/SFCGAL/-/archive/v1.3.10/SFCGAL-v1.3.10.tar.gz -O SFCGAL.tar.gz && \
|
RUN wget https://gitlab.com/Oslandia/SFCGAL/-/archive/v1.3.10/SFCGAL-v1.3.10.tar.gz -O SFCGAL.tar.gz && \
|
||||||
echo "4e39b3b2adada6254a7bdba6d297bb28e1a9835a9f879b74f37e2dab70203232 SFCGAL.tar.gz" | sha256sum --check && \
|
echo "4e39b3b2adada6254a7bdba6d297bb28e1a9835a9f879b74f37e2dab70203232 SFCGAL.tar.gz" | sha256sum --check && \
|
||||||
mkdir sfcgal-src && cd sfcgal-src && tar xvzf ../SFCGAL.tar.gz --strip-components=1 -C . && \
|
mkdir sfcgal-src && cd sfcgal-src && tar xzf ../SFCGAL.tar.gz --strip-components=1 -C . && \
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release . && make -j $(getconf _NPROCESSORS_ONLN) && \
|
cmake -DCMAKE_BUILD_TYPE=Release . && make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
DESTDIR=/sfcgal make install -j $(getconf _NPROCESSORS_ONLN) && \
|
DESTDIR=/sfcgal make install -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make clean && cp -R /sfcgal/* /
|
make clean && cp -R /sfcgal/* /
|
||||||
@@ -98,7 +98,7 @@ ENV PATH "/usr/local/pgsql/bin:$PATH"
|
|||||||
|
|
||||||
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.3.tar.gz -O postgis.tar.gz && \
|
RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.3.tar.gz -O postgis.tar.gz && \
|
||||||
echo "74eb356e3f85f14233791013360881b6748f78081cc688ff9d6f0f673a762d13 postgis.tar.gz" | sha256sum --check && \
|
echo "74eb356e3f85f14233791013360881b6748f78081cc688ff9d6f0f673a762d13 postgis.tar.gz" | sha256sum --check && \
|
||||||
mkdir postgis-src && cd postgis-src && tar xvzf ../postgis.tar.gz --strip-components=1 -C . && \
|
mkdir postgis-src && cd postgis-src && tar xzf ../postgis.tar.gz --strip-components=1 -C . && \
|
||||||
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
||||||
./autogen.sh && \
|
./autogen.sh && \
|
||||||
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
|
./configure --with-sfcgal=/usr/local/bin/sfcgal-config && \
|
||||||
@@ -124,7 +124,7 @@ RUN wget https://download.osgeo.org/postgis/source/postgis-3.3.3.tar.gz -O postg
|
|||||||
|
|
||||||
RUN wget https://github.com/pgRouting/pgrouting/archive/v3.4.2.tar.gz -O pgrouting.tar.gz && \
|
RUN wget https://github.com/pgRouting/pgrouting/archive/v3.4.2.tar.gz -O pgrouting.tar.gz && \
|
||||||
echo "cac297c07d34460887c4f3b522b35c470138760fe358e351ad1db4edb6ee306e pgrouting.tar.gz" | sha256sum --check && \
|
echo "cac297c07d34460887c4f3b522b35c470138760fe358e351ad1db4edb6ee306e pgrouting.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgrouting-src && cd pgrouting-src && tar xvzf ../pgrouting.tar.gz --strip-components=1 -C . && \
|
mkdir pgrouting-src && cd pgrouting-src && tar xzf ../pgrouting.tar.gz --strip-components=1 -C . && \
|
||||||
mkdir build && cd build && \
|
mkdir build && cd build && \
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release .. && \
|
cmake -DCMAKE_BUILD_TYPE=Release .. && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
@@ -149,7 +149,7 @@ RUN apt update && \
|
|||||||
|
|
||||||
RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.10.tar.gz -O plv8.tar.gz && \
|
RUN wget https://github.com/plv8/plv8/archive/refs/tags/v3.1.10.tar.gz -O plv8.tar.gz && \
|
||||||
echo "7096c3290928561f0d4901b7a52794295dc47f6303102fae3f8e42dd575ad97d plv8.tar.gz" | sha256sum --check && \
|
echo "7096c3290928561f0d4901b7a52794295dc47f6303102fae3f8e42dd575ad97d plv8.tar.gz" | sha256sum --check && \
|
||||||
mkdir plv8-src && cd plv8-src && tar xvzf ../plv8.tar.gz --strip-components=1 -C . && \
|
mkdir plv8-src && cd plv8-src && tar xzf ../plv8.tar.gz --strip-components=1 -C . && \
|
||||||
# generate and copy upgrade scripts
|
# generate and copy upgrade scripts
|
||||||
mkdir -p upgrade && ./generate_upgrade.sh 3.1.10 && \
|
mkdir -p upgrade && ./generate_upgrade.sh 3.1.10 && \
|
||||||
cp upgrade/* /usr/local/pgsql/share/extension/ && \
|
cp upgrade/* /usr/local/pgsql/share/extension/ && \
|
||||||
@@ -194,7 +194,7 @@ RUN case "$(uname -m)" in \
|
|||||||
|
|
||||||
RUN wget https://github.com/uber/h3/archive/refs/tags/v4.1.0.tar.gz -O h3.tar.gz && \
|
RUN wget https://github.com/uber/h3/archive/refs/tags/v4.1.0.tar.gz -O h3.tar.gz && \
|
||||||
echo "ec99f1f5974846bde64f4513cf8d2ea1b8d172d2218ab41803bf6a63532272bc h3.tar.gz" | sha256sum --check && \
|
echo "ec99f1f5974846bde64f4513cf8d2ea1b8d172d2218ab41803bf6a63532272bc h3.tar.gz" | sha256sum --check && \
|
||||||
mkdir h3-src && cd h3-src && tar xvzf ../h3.tar.gz --strip-components=1 -C . && \
|
mkdir h3-src && cd h3-src && tar xzf ../h3.tar.gz --strip-components=1 -C . && \
|
||||||
mkdir build && cd build && \
|
mkdir build && cd build && \
|
||||||
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
cmake .. -DCMAKE_BUILD_TYPE=Release && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
@@ -204,7 +204,7 @@ RUN wget https://github.com/uber/h3/archive/refs/tags/v4.1.0.tar.gz -O h3.tar.gz
|
|||||||
|
|
||||||
RUN wget https://github.com/zachasme/h3-pg/archive/refs/tags/v4.1.3.tar.gz -O h3-pg.tar.gz && \
|
RUN wget https://github.com/zachasme/h3-pg/archive/refs/tags/v4.1.3.tar.gz -O h3-pg.tar.gz && \
|
||||||
echo "5c17f09a820859ffe949f847bebf1be98511fb8f1bd86f94932512c00479e324 h3-pg.tar.gz" | sha256sum --check && \
|
echo "5c17f09a820859ffe949f847bebf1be98511fb8f1bd86f94932512c00479e324 h3-pg.tar.gz" | sha256sum --check && \
|
||||||
mkdir h3-pg-src && cd h3-pg-src && tar xvzf ../h3-pg.tar.gz --strip-components=1 -C . && \
|
mkdir h3-pg-src && cd h3-pg-src && tar xzf ../h3-pg.tar.gz --strip-components=1 -C . && \
|
||||||
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
export PATH="/usr/local/pgsql/bin:$PATH" && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
@@ -222,7 +222,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -O postgresql-unit.tar.gz && \
|
RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -O postgresql-unit.tar.gz && \
|
||||||
echo "411d05beeb97e5a4abf17572bfcfbb5a68d98d1018918feff995f6ee3bb03e79 postgresql-unit.tar.gz" | sha256sum --check && \
|
echo "411d05beeb97e5a4abf17572bfcfbb5a68d98d1018918feff995f6ee3bb03e79 postgresql-unit.tar.gz" | sha256sum --check && \
|
||||||
mkdir postgresql-unit-src && cd postgresql-unit-src && tar xvzf ../postgresql-unit.tar.gz --strip-components=1 -C . && \
|
mkdir postgresql-unit-src && cd postgresql-unit-src && tar xzf ../postgresql-unit.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
# unit extension's "create extension" script relies on absolute install path to fill some reference tables.
|
# unit extension's "create extension" script relies on absolute install path to fill some reference tables.
|
||||||
@@ -241,11 +241,17 @@ RUN wget https://github.com/df7cb/postgresql-unit/archive/refs/tags/7.7.tar.gz -
|
|||||||
FROM build-deps AS vector-pg-build
|
FROM build-deps AS vector-pg-build
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
|
|
||||||
RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.5.1.tar.gz -O pgvector.tar.gz && \
|
COPY patches/pgvector.patch /pgvector.patch
|
||||||
echo "cc7a8e034a96e30a819911ac79d32f6bc47bdd1aa2de4d7d4904e26b83209dc8 pgvector.tar.gz" | sha256sum --check && \
|
|
||||||
mkdir pgvector-src && cd pgvector-src && tar xvzf ../pgvector.tar.gz --strip-components=1 -C . && \
|
# By default, pgvector Makefile uses `-march=native`. We don't want that,
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
# because we build the images on different machines than where we run them.
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
# Pass OPTFLAGS="" to remove it.
|
||||||
|
RUN wget https://github.com/pgvector/pgvector/archive/refs/tags/v0.7.2.tar.gz -O pgvector.tar.gz && \
|
||||||
|
echo "617fba855c9bcb41a2a9bc78a78567fd2e147c72afd5bf9d37b31b9591632b30 pgvector.tar.gz" | sha256sum --check && \
|
||||||
|
mkdir pgvector-src && cd pgvector-src && tar xzf ../pgvector.tar.gz --strip-components=1 -C . && \
|
||||||
|
patch -p1 < /pgvector.patch && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) OPTFLAGS="" PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
|
make -j $(getconf _NPROCESSORS_ONLN) OPTFLAGS="" install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/vector.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/vector.control
|
||||||
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
@@ -260,7 +266,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
# 9742dab1b2f297ad3811120db7b21451bca2d3c9 made on 13/11/2021
|
# 9742dab1b2f297ad3811120db7b21451bca2d3c9 made on 13/11/2021
|
||||||
RUN wget https://github.com/michelp/pgjwt/archive/9742dab1b2f297ad3811120db7b21451bca2d3c9.tar.gz -O pgjwt.tar.gz && \
|
RUN wget https://github.com/michelp/pgjwt/archive/9742dab1b2f297ad3811120db7b21451bca2d3c9.tar.gz -O pgjwt.tar.gz && \
|
||||||
echo "cfdefb15007286f67d3d45510f04a6a7a495004be5b3aecb12cda667e774203f pgjwt.tar.gz" | sha256sum --check && \
|
echo "cfdefb15007286f67d3d45510f04a6a7a495004be5b3aecb12cda667e774203f pgjwt.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgjwt-src && cd pgjwt-src && tar xvzf ../pgjwt.tar.gz --strip-components=1 -C . && \
|
mkdir pgjwt-src && cd pgjwt-src && tar xzf ../pgjwt.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgjwt.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgjwt.control
|
||||||
|
|
||||||
@@ -275,7 +281,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/HypoPG/hypopg/archive/refs/tags/1.4.0.tar.gz -O hypopg.tar.gz && \
|
RUN wget https://github.com/HypoPG/hypopg/archive/refs/tags/1.4.0.tar.gz -O hypopg.tar.gz && \
|
||||||
echo "0821011743083226fc9b813c1f2ef5897a91901b57b6bea85a78e466187c6819 hypopg.tar.gz" | sha256sum --check && \
|
echo "0821011743083226fc9b813c1f2ef5897a91901b57b6bea85a78e466187c6819 hypopg.tar.gz" | sha256sum --check && \
|
||||||
mkdir hypopg-src && cd hypopg-src && tar xvzf ../hypopg.tar.gz --strip-components=1 -C . && \
|
mkdir hypopg-src && cd hypopg-src && tar xzf ../hypopg.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hypopg.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hypopg.control
|
||||||
@@ -291,7 +297,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/iCyberon/pg_hashids/archive/refs/tags/v1.2.1.tar.gz -O pg_hashids.tar.gz && \
|
RUN wget https://github.com/iCyberon/pg_hashids/archive/refs/tags/v1.2.1.tar.gz -O pg_hashids.tar.gz && \
|
||||||
echo "74576b992d9277c92196dd8d816baa2cc2d8046fe102f3dcd7f3c3febed6822a pg_hashids.tar.gz" | sha256sum --check && \
|
echo "74576b992d9277c92196dd8d816baa2cc2d8046fe102f3dcd7f3c3febed6822a pg_hashids.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_hashids-src && cd pg_hashids-src && tar xvzf ../pg_hashids.tar.gz --strip-components=1 -C . && \
|
mkdir pg_hashids-src && cd pg_hashids-src && tar xzf ../pg_hashids.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_hashids.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_hashids.control
|
||||||
@@ -307,7 +313,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/postgrespro/rum/archive/refs/tags/1.3.13.tar.gz -O rum.tar.gz && \
|
RUN wget https://github.com/postgrespro/rum/archive/refs/tags/1.3.13.tar.gz -O rum.tar.gz && \
|
||||||
echo "6ab370532c965568df6210bd844ac6ba649f53055e48243525b0b7e5c4d69a7d rum.tar.gz" | sha256sum --check && \
|
echo "6ab370532c965568df6210bd844ac6ba649f53055e48243525b0b7e5c4d69a7d rum.tar.gz" | sha256sum --check && \
|
||||||
mkdir rum-src && cd rum-src && tar xvzf ../rum.tar.gz --strip-components=1 -C . && \
|
mkdir rum-src && cd rum-src && tar xzf ../rum.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rum.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/rum.control
|
||||||
@@ -323,7 +329,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/theory/pgtap/archive/refs/tags/v1.2.0.tar.gz -O pgtap.tar.gz && \
|
RUN wget https://github.com/theory/pgtap/archive/refs/tags/v1.2.0.tar.gz -O pgtap.tar.gz && \
|
||||||
echo "9c7c3de67ea41638e14f06da5da57bac6f5bd03fea05c165a0ec862205a5c052 pgtap.tar.gz" | sha256sum --check && \
|
echo "9c7c3de67ea41638e14f06da5da57bac6f5bd03fea05c165a0ec862205a5c052 pgtap.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgtap-src && cd pgtap-src && tar xvzf ../pgtap.tar.gz --strip-components=1 -C . && \
|
mkdir pgtap-src && cd pgtap-src && tar xzf ../pgtap.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgtap.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pgtap.control
|
||||||
@@ -339,7 +345,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/RhodiumToad/ip4r/archive/refs/tags/2.4.2.tar.gz -O ip4r.tar.gz && \
|
RUN wget https://github.com/RhodiumToad/ip4r/archive/refs/tags/2.4.2.tar.gz -O ip4r.tar.gz && \
|
||||||
echo "0f7b1f159974f49a47842a8ab6751aecca1ed1142b6d5e38d81b064b2ead1b4b ip4r.tar.gz" | sha256sum --check && \
|
echo "0f7b1f159974f49a47842a8ab6751aecca1ed1142b6d5e38d81b064b2ead1b4b ip4r.tar.gz" | sha256sum --check && \
|
||||||
mkdir ip4r-src && cd ip4r-src && tar xvzf ../ip4r.tar.gz --strip-components=1 -C . && \
|
mkdir ip4r-src && cd ip4r-src && tar xzf ../ip4r.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/ip4r.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/ip4r.control
|
||||||
@@ -355,7 +361,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/dimitri/prefix/archive/refs/tags/v1.2.10.tar.gz -O prefix.tar.gz && \
|
RUN wget https://github.com/dimitri/prefix/archive/refs/tags/v1.2.10.tar.gz -O prefix.tar.gz && \
|
||||||
echo "4342f251432a5f6fb05b8597139d3ccde8dcf87e8ca1498e7ee931ca057a8575 prefix.tar.gz" | sha256sum --check && \
|
echo "4342f251432a5f6fb05b8597139d3ccde8dcf87e8ca1498e7ee931ca057a8575 prefix.tar.gz" | sha256sum --check && \
|
||||||
mkdir prefix-src && cd prefix-src && tar xvzf ../prefix.tar.gz --strip-components=1 -C . && \
|
mkdir prefix-src && cd prefix-src && tar xzf ../prefix.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/prefix.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/prefix.control
|
||||||
@@ -371,7 +377,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/citusdata/postgresql-hll/archive/refs/tags/v2.18.tar.gz -O hll.tar.gz && \
|
RUN wget https://github.com/citusdata/postgresql-hll/archive/refs/tags/v2.18.tar.gz -O hll.tar.gz && \
|
||||||
echo "e2f55a6f4c4ab95ee4f1b4a2b73280258c5136b161fe9d059559556079694f0e hll.tar.gz" | sha256sum --check && \
|
echo "e2f55a6f4c4ab95ee4f1b4a2b73280258c5136b161fe9d059559556079694f0e hll.tar.gz" | sha256sum --check && \
|
||||||
mkdir hll-src && cd hll-src && tar xvzf ../hll.tar.gz --strip-components=1 -C . && \
|
mkdir hll-src && cd hll-src && tar xzf ../hll.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hll.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/hll.control
|
||||||
@@ -387,7 +393,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
|
|
||||||
RUN wget https://github.com/okbob/plpgsql_check/archive/refs/tags/v2.5.3.tar.gz -O plpgsql_check.tar.gz && \
|
RUN wget https://github.com/okbob/plpgsql_check/archive/refs/tags/v2.5.3.tar.gz -O plpgsql_check.tar.gz && \
|
||||||
echo "6631ec3e7fb3769eaaf56e3dfedb829aa761abf163d13dba354b4c218508e1c0 plpgsql_check.tar.gz" | sha256sum --check && \
|
echo "6631ec3e7fb3769eaaf56e3dfedb829aa761abf163d13dba354b4c218508e1c0 plpgsql_check.tar.gz" | sha256sum --check && \
|
||||||
mkdir plpgsql_check-src && cd plpgsql_check-src && tar xvzf ../plpgsql_check.tar.gz --strip-components=1 -C . && \
|
mkdir plpgsql_check-src && cd plpgsql_check-src && tar xzf ../plpgsql_check.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config USE_PGXS=1 && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plpgsql_check.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/plpgsql_check.control
|
||||||
@@ -418,7 +424,7 @@ RUN case "${PG_VERSION}" in \
|
|||||||
apt-get install -y cmake && \
|
apt-get install -y cmake && \
|
||||||
wget https://github.com/timescale/timescaledb/archive/refs/tags/${TIMESCALEDB_VERSION}.tar.gz -O timescaledb.tar.gz && \
|
wget https://github.com/timescale/timescaledb/archive/refs/tags/${TIMESCALEDB_VERSION}.tar.gz -O timescaledb.tar.gz && \
|
||||||
echo "${TIMESCALEDB_CHECKSUM} timescaledb.tar.gz" | sha256sum --check && \
|
echo "${TIMESCALEDB_CHECKSUM} timescaledb.tar.gz" | sha256sum --check && \
|
||||||
mkdir timescaledb-src && cd timescaledb-src && tar xvzf ../timescaledb.tar.gz --strip-components=1 -C . && \
|
mkdir timescaledb-src && cd timescaledb-src && tar xzf ../timescaledb.tar.gz --strip-components=1 -C . && \
|
||||||
./bootstrap -DSEND_TELEMETRY_DEFAULT:BOOL=OFF -DUSE_TELEMETRY:BOOL=OFF -DAPACHE_ONLY:BOOL=ON -DCMAKE_BUILD_TYPE=Release && \
|
./bootstrap -DSEND_TELEMETRY_DEFAULT:BOOL=OFF -DUSE_TELEMETRY:BOOL=OFF -DAPACHE_ONLY:BOOL=ON -DCMAKE_BUILD_TYPE=Release && \
|
||||||
cd build && \
|
cd build && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
@@ -456,36 +462,11 @@ RUN case "${PG_VERSION}" in \
|
|||||||
esac && \
|
esac && \
|
||||||
wget https://github.com/ossc-db/pg_hint_plan/archive/refs/tags/REL${PG_HINT_PLAN_VERSION}.tar.gz -O pg_hint_plan.tar.gz && \
|
wget https://github.com/ossc-db/pg_hint_plan/archive/refs/tags/REL${PG_HINT_PLAN_VERSION}.tar.gz -O pg_hint_plan.tar.gz && \
|
||||||
echo "${PG_HINT_PLAN_CHECKSUM} pg_hint_plan.tar.gz" | sha256sum --check && \
|
echo "${PG_HINT_PLAN_CHECKSUM} pg_hint_plan.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_hint_plan-src && cd pg_hint_plan-src && tar xvzf ../pg_hint_plan.tar.gz --strip-components=1 -C . && \
|
mkdir pg_hint_plan-src && cd pg_hint_plan-src && tar xzf ../pg_hint_plan.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make install -j $(getconf _NPROCESSORS_ONLN) && \
|
make install -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_hint_plan.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_hint_plan.control
|
||||||
|
|
||||||
#########################################################################################
|
|
||||||
#
|
|
||||||
# Layer "kq-imcx-pg-build"
|
|
||||||
# compile kq_imcx extension
|
|
||||||
#
|
|
||||||
#########################################################################################
|
|
||||||
FROM build-deps AS kq-imcx-pg-build
|
|
||||||
COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
|
|
||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y git libgtk2.0-dev libpq-dev libpam-dev libxslt-dev libkrb5-dev cmake && \
|
|
||||||
wget https://github.com/ketteq-neon/postgres-exts/archive/e0bd1a9d9313d7120c1b9c7bb15c48c0dede4c4e.tar.gz -O kq_imcx.tar.gz && \
|
|
||||||
echo "dc93a97ff32d152d32737ba7e196d9687041cda15e58ab31344c2f2de8855336 kq_imcx.tar.gz" | sha256sum --check && \
|
|
||||||
mkdir kq_imcx-src && cd kq_imcx-src && tar xvzf ../kq_imcx.tar.gz --strip-components=1 -C . && \
|
|
||||||
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
|
||||||
mkdir build && cd build && \
|
|
||||||
cmake -DCMAKE_BUILD_TYPE=Release .. && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/kq_imcx.control && \
|
|
||||||
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /after.txt &&\
|
|
||||||
mkdir -p /extensions/kq_imcx && cp /usr/local/pgsql/share/extension/kq_imcx.control /extensions/kq_imcx && \
|
|
||||||
sort -o /before.txt /before.txt && sort -o /after.txt /after.txt && \
|
|
||||||
comm -13 /before.txt /after.txt | tar --directory=/usr/local/pgsql --zstd -cf /extensions/kq_imcx.tar.zst -T -
|
|
||||||
|
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
#
|
#
|
||||||
@@ -499,7 +480,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/citusdata/pg_cron/archive/refs/tags/v1.6.0.tar.gz -O pg_cron.tar.gz && \
|
RUN wget https://github.com/citusdata/pg_cron/archive/refs/tags/v1.6.0.tar.gz -O pg_cron.tar.gz && \
|
||||||
echo "383a627867d730222c272bfd25cd5e151c578d73f696d32910c7db8c665cc7db pg_cron.tar.gz" | sha256sum --check && \
|
echo "383a627867d730222c272bfd25cd5e151c578d73f696d32910c7db8c665cc7db pg_cron.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_cron-src && cd pg_cron-src && tar xvzf ../pg_cron.tar.gz --strip-components=1 -C . && \
|
mkdir pg_cron-src && cd pg_cron-src && tar xzf ../pg_cron.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_cron.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_cron.control
|
||||||
@@ -525,7 +506,7 @@ RUN apt-get update && \
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:/usr/local/pgsql/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:/usr/local/pgsql/:$PATH"
|
||||||
RUN wget https://github.com/rdkit/rdkit/archive/refs/tags/Release_2023_03_3.tar.gz -O rdkit.tar.gz && \
|
RUN wget https://github.com/rdkit/rdkit/archive/refs/tags/Release_2023_03_3.tar.gz -O rdkit.tar.gz && \
|
||||||
echo "bdbf9a2e6988526bfeb8c56ce3cdfe2998d60ac289078e2215374288185e8c8d rdkit.tar.gz" | sha256sum --check && \
|
echo "bdbf9a2e6988526bfeb8c56ce3cdfe2998d60ac289078e2215374288185e8c8d rdkit.tar.gz" | sha256sum --check && \
|
||||||
mkdir rdkit-src && cd rdkit-src && tar xvzf ../rdkit.tar.gz --strip-components=1 -C . && \
|
mkdir rdkit-src && cd rdkit-src && tar xzf ../rdkit.tar.gz --strip-components=1 -C . && \
|
||||||
cmake \
|
cmake \
|
||||||
-D RDK_BUILD_CAIRO_SUPPORT=OFF \
|
-D RDK_BUILD_CAIRO_SUPPORT=OFF \
|
||||||
-D RDK_BUILD_INCHI_SUPPORT=ON \
|
-D RDK_BUILD_INCHI_SUPPORT=ON \
|
||||||
@@ -565,7 +546,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/fboulnois/pg_uuidv7/archive/refs/tags/v1.0.1.tar.gz -O pg_uuidv7.tar.gz && \
|
RUN wget https://github.com/fboulnois/pg_uuidv7/archive/refs/tags/v1.0.1.tar.gz -O pg_uuidv7.tar.gz && \
|
||||||
echo "0d0759ab01b7fb23851ecffb0bce27822e1868a4a5819bfd276101c716637a7a pg_uuidv7.tar.gz" | sha256sum --check && \
|
echo "0d0759ab01b7fb23851ecffb0bce27822e1868a4a5819bfd276101c716637a7a pg_uuidv7.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_uuidv7-src && cd pg_uuidv7-src && tar xvzf ../pg_uuidv7.tar.gz --strip-components=1 -C . && \
|
mkdir pg_uuidv7-src && cd pg_uuidv7-src && tar xzf ../pg_uuidv7.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_uuidv7.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_uuidv7.control
|
||||||
@@ -582,7 +563,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/ChenHuajun/pg_roaringbitmap/archive/refs/tags/v0.5.4.tar.gz -O pg_roaringbitmap.tar.gz && \
|
RUN wget https://github.com/ChenHuajun/pg_roaringbitmap/archive/refs/tags/v0.5.4.tar.gz -O pg_roaringbitmap.tar.gz && \
|
||||||
echo "b75201efcb1c2d1b014ec4ae6a22769cc7a224e6e406a587f5784a37b6b5a2aa pg_roaringbitmap.tar.gz" | sha256sum --check && \
|
echo "b75201efcb1c2d1b014ec4ae6a22769cc7a224e6e406a587f5784a37b6b5a2aa pg_roaringbitmap.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_roaringbitmap-src && cd pg_roaringbitmap-src && tar xvzf ../pg_roaringbitmap.tar.gz --strip-components=1 -C . && \
|
mkdir pg_roaringbitmap-src && cd pg_roaringbitmap-src && tar xzf ../pg_roaringbitmap.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/roaringbitmap.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/roaringbitmap.control
|
||||||
@@ -599,7 +580,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/theory/pg-semver/archive/refs/tags/v0.32.1.tar.gz -O pg_semver.tar.gz && \
|
RUN wget https://github.com/theory/pg-semver/archive/refs/tags/v0.32.1.tar.gz -O pg_semver.tar.gz && \
|
||||||
echo "fbdaf7512026d62eec03fad8687c15ed509b6ba395bff140acd63d2e4fbe25d7 pg_semver.tar.gz" | sha256sum --check && \
|
echo "fbdaf7512026d62eec03fad8687c15ed509b6ba395bff140acd63d2e4fbe25d7 pg_semver.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_semver-src && cd pg_semver-src && tar xvzf ../pg_semver.tar.gz --strip-components=1 -C . && \
|
mkdir pg_semver-src && cd pg_semver-src && tar xzf ../pg_semver.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/semver.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/semver.control
|
||||||
@@ -625,7 +606,7 @@ RUN case "${PG_VERSION}" in \
|
|||||||
esac && \
|
esac && \
|
||||||
wget https://github.com/neondatabase/pg_embedding/archive/refs/tags/${PG_EMBEDDING_VERSION}.tar.gz -O pg_embedding.tar.gz && \
|
wget https://github.com/neondatabase/pg_embedding/archive/refs/tags/${PG_EMBEDDING_VERSION}.tar.gz -O pg_embedding.tar.gz && \
|
||||||
echo "${PG_EMBEDDING_CHECKSUM} pg_embedding.tar.gz" | sha256sum --check && \
|
echo "${PG_EMBEDDING_CHECKSUM} pg_embedding.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_embedding-src && cd pg_embedding-src && tar xvzf ../pg_embedding.tar.gz --strip-components=1 -C . && \
|
mkdir pg_embedding-src && cd pg_embedding-src && tar xzf ../pg_embedding.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install
|
make -j $(getconf _NPROCESSORS_ONLN) install
|
||||||
|
|
||||||
@@ -641,7 +622,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/neondatabase/postgresql_anonymizer/archive/refs/tags/neon_1.1.1.tar.gz -O pg_anon.tar.gz && \
|
RUN wget https://github.com/neondatabase/postgresql_anonymizer/archive/refs/tags/neon_1.1.1.tar.gz -O pg_anon.tar.gz && \
|
||||||
echo "321ea8d5c1648880aafde850a2c576e4a9e7b9933a34ce272efc839328999fa9 pg_anon.tar.gz" | sha256sum --check && \
|
echo "321ea8d5c1648880aafde850a2c576e4a9e7b9933a34ce272efc839328999fa9 pg_anon.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_anon-src && cd pg_anon-src && tar xvzf ../pg_anon.tar.gz --strip-components=1 -C . && \
|
mkdir pg_anon-src && cd pg_anon-src && tar xzf ../pg_anon.tar.gz --strip-components=1 -C . && \
|
||||||
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
find /usr/local/pgsql -type f | sed 's|^/usr/local/pgsql/||' > /before.txt &&\
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
make -j $(getconf _NPROCESSORS_ONLN) install PG_CONFIG=/usr/local/pgsql/bin/pg_config && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/anon.control && \
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/anon.control && \
|
||||||
@@ -690,7 +671,7 @@ ARG PG_VERSION
|
|||||||
|
|
||||||
RUN wget https://github.com/supabase/pg_jsonschema/archive/refs/tags/v0.2.0.tar.gz -O pg_jsonschema.tar.gz && \
|
RUN wget https://github.com/supabase/pg_jsonschema/archive/refs/tags/v0.2.0.tar.gz -O pg_jsonschema.tar.gz && \
|
||||||
echo "9118fc508a6e231e7a39acaa6f066fcd79af17a5db757b47d2eefbe14f7794f0 pg_jsonschema.tar.gz" | sha256sum --check && \
|
echo "9118fc508a6e231e7a39acaa6f066fcd79af17a5db757b47d2eefbe14f7794f0 pg_jsonschema.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_jsonschema-src && cd pg_jsonschema-src && tar xvzf ../pg_jsonschema.tar.gz --strip-components=1 -C . && \
|
mkdir pg_jsonschema-src && cd pg_jsonschema-src && tar xzf ../pg_jsonschema.tar.gz --strip-components=1 -C . && \
|
||||||
sed -i 's/pgrx = "0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
sed -i 's/pgrx = "0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
||||||
cargo pgrx install --release && \
|
cargo pgrx install --release && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_jsonschema.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_jsonschema.control
|
||||||
@@ -707,7 +688,7 @@ ARG PG_VERSION
|
|||||||
|
|
||||||
RUN wget https://github.com/supabase/pg_graphql/archive/refs/tags/v1.4.0.tar.gz -O pg_graphql.tar.gz && \
|
RUN wget https://github.com/supabase/pg_graphql/archive/refs/tags/v1.4.0.tar.gz -O pg_graphql.tar.gz && \
|
||||||
echo "bd8dc7230282b3efa9ae5baf053a54151ed0e66881c7c53750e2d0c765776edc pg_graphql.tar.gz" | sha256sum --check && \
|
echo "bd8dc7230282b3efa9ae5baf053a54151ed0e66881c7c53750e2d0c765776edc pg_graphql.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_graphql-src && cd pg_graphql-src && tar xvzf ../pg_graphql.tar.gz --strip-components=1 -C . && \
|
mkdir pg_graphql-src && cd pg_graphql-src && tar xzf ../pg_graphql.tar.gz --strip-components=1 -C . && \
|
||||||
sed -i 's/pgrx = "=0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
sed -i 's/pgrx = "=0.10.2"/pgrx = { version = "0.10.2", features = [ "unsafe-postgres" ] }/g' Cargo.toml && \
|
||||||
cargo pgrx install --release && \
|
cargo pgrx install --release && \
|
||||||
# it's needed to enable extension because it uses untrusted C language
|
# it's needed to enable extension because it uses untrusted C language
|
||||||
@@ -727,7 +708,7 @@ ARG PG_VERSION
|
|||||||
# 26806147b17b60763039c6a6878884c41a262318 made on 26/09/2023
|
# 26806147b17b60763039c6a6878884c41a262318 made on 26/09/2023
|
||||||
RUN wget https://github.com/kelvich/pg_tiktoken/archive/26806147b17b60763039c6a6878884c41a262318.tar.gz -O pg_tiktoken.tar.gz && \
|
RUN wget https://github.com/kelvich/pg_tiktoken/archive/26806147b17b60763039c6a6878884c41a262318.tar.gz -O pg_tiktoken.tar.gz && \
|
||||||
echo "e64e55aaa38c259512d3e27c572da22c4637418cf124caba904cd50944e5004e pg_tiktoken.tar.gz" | sha256sum --check && \
|
echo "e64e55aaa38c259512d3e27c572da22c4637418cf124caba904cd50944e5004e pg_tiktoken.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_tiktoken-src && cd pg_tiktoken-src && tar xvzf ../pg_tiktoken.tar.gz --strip-components=1 -C . && \
|
mkdir pg_tiktoken-src && cd pg_tiktoken-src && tar xzf ../pg_tiktoken.tar.gz --strip-components=1 -C . && \
|
||||||
cargo pgrx install --release && \
|
cargo pgrx install --release && \
|
||||||
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_tiktoken.control
|
echo "trusted = true" >> /usr/local/pgsql/share/extension/pg_tiktoken.control
|
||||||
|
|
||||||
@@ -743,7 +724,7 @@ ARG PG_VERSION
|
|||||||
|
|
||||||
RUN wget https://github.com/pksunkara/pgx_ulid/archive/refs/tags/v0.1.3.tar.gz -O pgx_ulid.tar.gz && \
|
RUN wget https://github.com/pksunkara/pgx_ulid/archive/refs/tags/v0.1.3.tar.gz -O pgx_ulid.tar.gz && \
|
||||||
echo "ee5db82945d2d9f2d15597a80cf32de9dca67b897f605beb830561705f12683c pgx_ulid.tar.gz" | sha256sum --check && \
|
echo "ee5db82945d2d9f2d15597a80cf32de9dca67b897f605beb830561705f12683c pgx_ulid.tar.gz" | sha256sum --check && \
|
||||||
mkdir pgx_ulid-src && cd pgx_ulid-src && tar xvzf ../pgx_ulid.tar.gz --strip-components=1 -C . && \
|
mkdir pgx_ulid-src && cd pgx_ulid-src && tar xzf ../pgx_ulid.tar.gz --strip-components=1 -C . && \
|
||||||
echo "******************* Apply a patch for Postgres 16 support; delete in the next release ******************" && \
|
echo "******************* Apply a patch for Postgres 16 support; delete in the next release ******************" && \
|
||||||
wget https://github.com/pksunkara/pgx_ulid/commit/f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
wget https://github.com/pksunkara/pgx_ulid/commit/f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
||||||
patch -p1 < f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
patch -p1 < f84954cf63fc8c80d964ac970d9eceed3c791196.patch && \
|
||||||
@@ -765,7 +746,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/eulerto/wal2json/archive/refs/tags/wal2json_2_5.tar.gz && \
|
RUN wget https://github.com/eulerto/wal2json/archive/refs/tags/wal2json_2_5.tar.gz && \
|
||||||
echo "b516653575541cf221b99cf3f8be9b6821f6dbcfc125675c85f35090f824f00e wal2json_2_5.tar.gz" | sha256sum --check && \
|
echo "b516653575541cf221b99cf3f8be9b6821f6dbcfc125675c85f35090f824f00e wal2json_2_5.tar.gz" | sha256sum --check && \
|
||||||
mkdir wal2json-src && cd wal2json-src && tar xvzf ../wal2json_2_5.tar.gz --strip-components=1 -C . && \
|
mkdir wal2json-src && cd wal2json-src && tar xzf ../wal2json_2_5.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install
|
make -j $(getconf _NPROCESSORS_ONLN) install
|
||||||
|
|
||||||
@@ -781,7 +762,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/sraoss/pg_ivm/archive/refs/tags/v1.7.tar.gz -O pg_ivm.tar.gz && \
|
RUN wget https://github.com/sraoss/pg_ivm/archive/refs/tags/v1.7.tar.gz -O pg_ivm.tar.gz && \
|
||||||
echo "ebfde04f99203c7be4b0e873f91104090e2e83e5429c32ac242d00f334224d5e pg_ivm.tar.gz" | sha256sum --check && \
|
echo "ebfde04f99203c7be4b0e873f91104090e2e83e5429c32ac242d00f334224d5e pg_ivm.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_ivm-src && cd pg_ivm-src && tar xvzf ../pg_ivm.tar.gz --strip-components=1 -C . && \
|
mkdir pg_ivm-src && cd pg_ivm-src && tar xzf ../pg_ivm.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_ivm.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_ivm.control
|
||||||
@@ -798,7 +779,7 @@ COPY --from=pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
ENV PATH "/usr/local/pgsql/bin/:$PATH"
|
||||||
RUN wget https://github.com/pgpartman/pg_partman/archive/refs/tags/v5.0.1.tar.gz -O pg_partman.tar.gz && \
|
RUN wget https://github.com/pgpartman/pg_partman/archive/refs/tags/v5.0.1.tar.gz -O pg_partman.tar.gz && \
|
||||||
echo "75b541733a9659a6c90dbd40fccb904a630a32880a6e3044d0c4c5f4c8a65525 pg_partman.tar.gz" | sha256sum --check && \
|
echo "75b541733a9659a6c90dbd40fccb904a630a32880a6e3044d0c4c5f4c8a65525 pg_partman.tar.gz" | sha256sum --check && \
|
||||||
mkdir pg_partman-src && cd pg_partman-src && tar xvzf ../pg_partman.tar.gz --strip-components=1 -C . && \
|
mkdir pg_partman-src && cd pg_partman-src && tar xzf ../pg_partman.tar.gz --strip-components=1 -C . && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) && \
|
make -j $(getconf _NPROCESSORS_ONLN) && \
|
||||||
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
make -j $(getconf _NPROCESSORS_ONLN) install && \
|
||||||
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_partman.control
|
echo 'trusted = true' >> /usr/local/pgsql/share/extension/pg_partman.control
|
||||||
@@ -834,7 +815,6 @@ COPY --from=hll-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|||||||
COPY --from=plpgsql-check-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=plpgsql-check-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=timescaledb-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=timescaledb-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=pg-hint-plan-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=pg-hint-plan-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=kq-imcx-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
|
||||||
COPY --from=pg-cron-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=pg-cron-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=pg-pgx-ulid-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=pg-pgx-ulid-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
COPY --from=rdkit-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
COPY --from=rdkit-pg-build /usr/local/pgsql/ /usr/local/pgsql/
|
||||||
@@ -922,6 +902,68 @@ RUN rm -r /usr/local/pgsql/include
|
|||||||
# if they were to be used by other libraries.
|
# if they were to be used by other libraries.
|
||||||
RUN rm /usr/local/pgsql/lib/lib*.a
|
RUN rm /usr/local/pgsql/lib/lib*.a
|
||||||
|
|
||||||
|
|
||||||
|
#########################################################################################
|
||||||
|
#
|
||||||
|
# Layer neon-pg-ext-test
|
||||||
|
#
|
||||||
|
#########################################################################################
|
||||||
|
|
||||||
|
FROM neon-pg-ext-build AS neon-pg-ext-test
|
||||||
|
ARG PG_VERSION
|
||||||
|
RUN mkdir /ext-src
|
||||||
|
|
||||||
|
#COPY --from=postgis-build /postgis.tar.gz /ext-src/
|
||||||
|
#COPY --from=postgis-build /sfcgal/* /usr
|
||||||
|
COPY --from=plv8-build /plv8.tar.gz /ext-src/
|
||||||
|
COPY --from=h3-pg-build /h3-pg.tar.gz /ext-src/
|
||||||
|
COPY --from=unit-pg-build /postgresql-unit.tar.gz /ext-src/
|
||||||
|
COPY --from=vector-pg-build /pgvector.tar.gz /ext-src/
|
||||||
|
COPY --from=vector-pg-build /pgvector.patch /ext-src/
|
||||||
|
COPY --from=pgjwt-pg-build /pgjwt.tar.gz /ext-src
|
||||||
|
#COPY --from=pg-jsonschema-pg-build /home/nonroot/pg_jsonschema.tar.gz /ext-src
|
||||||
|
#COPY --from=pg-graphql-pg-build /home/nonroot/pg_graphql.tar.gz /ext-src
|
||||||
|
#COPY --from=pg-tiktoken-pg-build /home/nonroot/pg_tiktoken.tar.gz /ext-src
|
||||||
|
COPY --from=hypopg-pg-build /hypopg.tar.gz /ext-src
|
||||||
|
COPY --from=pg-hashids-pg-build /pg_hashids.tar.gz /ext-src
|
||||||
|
#COPY --from=rum-pg-build /rum.tar.gz /ext-src
|
||||||
|
#COPY --from=pgtap-pg-build /pgtap.tar.gz /ext-src
|
||||||
|
COPY --from=ip4r-pg-build /ip4r.tar.gz /ext-src
|
||||||
|
COPY --from=prefix-pg-build /prefix.tar.gz /ext-src
|
||||||
|
COPY --from=hll-pg-build /hll.tar.gz /ext-src
|
||||||
|
COPY --from=plpgsql-check-pg-build /plpgsql_check.tar.gz /ext-src
|
||||||
|
#COPY --from=timescaledb-pg-build /timescaledb.tar.gz /ext-src
|
||||||
|
COPY --from=pg-hint-plan-pg-build /pg_hint_plan.tar.gz /ext-src
|
||||||
|
COPY patches/pg_hintplan.patch /ext-src
|
||||||
|
COPY --from=pg-cron-pg-build /pg_cron.tar.gz /ext-src
|
||||||
|
COPY patches/pg_cron.patch /ext-src
|
||||||
|
#COPY --from=pg-pgx-ulid-build /home/nonroot/pgx_ulid.tar.gz /ext-src
|
||||||
|
COPY --from=rdkit-pg-build /rdkit.tar.gz /ext-src
|
||||||
|
COPY --from=pg-uuidv7-pg-build /pg_uuidv7.tar.gz /ext-src
|
||||||
|
COPY --from=pg-roaringbitmap-pg-build /pg_roaringbitmap.tar.gz /ext-src
|
||||||
|
COPY --from=pg-semver-pg-build /pg_semver.tar.gz /ext-src
|
||||||
|
#COPY --from=pg-embedding-pg-build /home/nonroot/pg_embedding-src/ /ext-src
|
||||||
|
#COPY --from=wal2json-pg-build /wal2json_2_5.tar.gz /ext-src
|
||||||
|
COPY --from=pg-anon-pg-build /pg_anon.tar.gz /ext-src
|
||||||
|
COPY patches/pg_anon.patch /ext-src
|
||||||
|
COPY --from=pg-ivm-build /pg_ivm.tar.gz /ext-src
|
||||||
|
COPY --from=pg-partman-build /pg_partman.tar.gz /ext-src
|
||||||
|
RUN cd /ext-src/ && for f in *.tar.gz; \
|
||||||
|
do echo $f; dname=$(echo $f | sed 's/\.tar.*//')-src; \
|
||||||
|
rm -rf $dname; mkdir $dname; tar xzf $f --strip-components=1 -C $dname \
|
||||||
|
|| exit 1; rm -f $f; done
|
||||||
|
RUN cd /ext-src/pgvector-src && patch -p1 <../pgvector.patch
|
||||||
|
# cmake is required for the h3 test
|
||||||
|
RUN apt-get update && apt-get install -y cmake
|
||||||
|
RUN patch -p1 < /ext-src/pg_hintplan.patch
|
||||||
|
COPY --chmod=755 docker-compose/run-tests.sh /run-tests.sh
|
||||||
|
RUN patch -p1 </ext-src/pg_anon.patch
|
||||||
|
RUN patch -p1 </ext-src/pg_cron.patch
|
||||||
|
ENV PATH=/usr/local/pgsql/bin:$PATH
|
||||||
|
ENV PGHOST=compute
|
||||||
|
ENV PGPORT=55433
|
||||||
|
ENV PGUSER=cloud_admin
|
||||||
|
ENV PGDATABASE=postgres
|
||||||
#########################################################################################
|
#########################################################################################
|
||||||
#
|
#
|
||||||
# Final layer
|
# Final layer
|
||||||
@@ -944,6 +986,9 @@ RUN mkdir /var/db && useradd -m -d /var/db/postgres postgres && \
|
|||||||
COPY --from=postgres-cleanup-layer --chown=postgres /usr/local/pgsql /usr/local
|
COPY --from=postgres-cleanup-layer --chown=postgres /usr/local/pgsql /usr/local
|
||||||
COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-debug-size-lto/compute_ctl /usr/local/bin/compute_ctl
|
COPY --from=compute-tools --chown=postgres /home/nonroot/target/release-line-debug-size-lto/compute_ctl /usr/local/bin/compute_ctl
|
||||||
|
|
||||||
|
# Create remote extension download directory
|
||||||
|
RUN mkdir /usr/local/download_extensions && chown -R postgres:postgres /usr/local/download_extensions
|
||||||
|
|
||||||
# Install:
|
# Install:
|
||||||
# libreadline8 for psql
|
# libreadline8 for psql
|
||||||
# libicu67, locales for collations (including ICU and plpgsql_check)
|
# libicu67, locales for collations (including ICU and plpgsql_check)
|
||||||
|
|||||||
44
Makefile
44
Makefile
@@ -3,6 +3,9 @@ ROOT_PROJECT_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
|||||||
# Where to install Postgres, default is ./pg_install, maybe useful for package managers
|
# Where to install Postgres, default is ./pg_install, maybe useful for package managers
|
||||||
POSTGRES_INSTALL_DIR ?= $(ROOT_PROJECT_DIR)/pg_install/
|
POSTGRES_INSTALL_DIR ?= $(ROOT_PROJECT_DIR)/pg_install/
|
||||||
|
|
||||||
|
OPENSSL_PREFIX_DIR := /usr/local/openssl
|
||||||
|
ICU_PREFIX_DIR := /usr/local/icu
|
||||||
|
|
||||||
#
|
#
|
||||||
# We differentiate between release / debug build types using the BUILD_TYPE
|
# We differentiate between release / debug build types using the BUILD_TYPE
|
||||||
# environment variable.
|
# environment variable.
|
||||||
@@ -20,19 +23,31 @@ else
|
|||||||
$(error Bad build type '$(BUILD_TYPE)', see Makefile for options)
|
$(error Bad build type '$(BUILD_TYPE)', see Makefile for options)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifeq ($(shell test -e /home/nonroot/.docker_build && echo -n yes),yes)
|
||||||
|
# Exclude static build openssl, icu for local build (MacOS, Linux)
|
||||||
|
# Only keep for build type release and debug
|
||||||
|
PG_CFLAGS += -I$(OPENSSL_PREFIX_DIR)/include
|
||||||
|
PG_CONFIGURE_OPTS += --with-icu
|
||||||
|
PG_CONFIGURE_OPTS += ICU_CFLAGS='-I/$(ICU_PREFIX_DIR)/include -DU_STATIC_IMPLEMENTATION'
|
||||||
|
PG_CONFIGURE_OPTS += ICU_LIBS='-L$(ICU_PREFIX_DIR)/lib -L$(ICU_PREFIX_DIR)/lib64 -licui18n -licuuc -licudata -lstdc++ -Wl,-Bdynamic -lm'
|
||||||
|
PG_CONFIGURE_OPTS += LDFLAGS='-L$(OPENSSL_PREFIX_DIR)/lib -L$(OPENSSL_PREFIX_DIR)/lib64 -L$(ICU_PREFIX_DIR)/lib -L$(ICU_PREFIX_DIR)/lib64 -Wl,-Bstatic -lssl -lcrypto -Wl,-Bdynamic -lrt -lm -ldl -lpthread'
|
||||||
|
endif
|
||||||
|
|
||||||
UNAME_S := $(shell uname -s)
|
UNAME_S := $(shell uname -s)
|
||||||
ifeq ($(UNAME_S),Linux)
|
ifeq ($(UNAME_S),Linux)
|
||||||
# Seccomp BPF is only available for Linux
|
# Seccomp BPF is only available for Linux
|
||||||
PG_CONFIGURE_OPTS += --with-libseccomp
|
PG_CONFIGURE_OPTS += --with-libseccomp
|
||||||
else ifeq ($(UNAME_S),Darwin)
|
else ifeq ($(UNAME_S),Darwin)
|
||||||
# macOS with brew-installed openssl requires explicit paths
|
ifndef DISABLE_HOMEBREW
|
||||||
# It can be configured with OPENSSL_PREFIX variable
|
# macOS with brew-installed openssl requires explicit paths
|
||||||
OPENSSL_PREFIX ?= $(shell brew --prefix openssl@3)
|
# It can be configured with OPENSSL_PREFIX variable
|
||||||
PG_CONFIGURE_OPTS += --with-includes=$(OPENSSL_PREFIX)/include --with-libraries=$(OPENSSL_PREFIX)/lib
|
OPENSSL_PREFIX := $(shell brew --prefix openssl@3)
|
||||||
PG_CONFIGURE_OPTS += PKG_CONFIG_PATH=$(shell brew --prefix icu4c)/lib/pkgconfig
|
PG_CONFIGURE_OPTS += --with-includes=$(OPENSSL_PREFIX)/include --with-libraries=$(OPENSSL_PREFIX)/lib
|
||||||
# macOS already has bison and flex in the system, but they are old and result in postgres-v14 target failure
|
PG_CONFIGURE_OPTS += PKG_CONFIG_PATH=$(shell brew --prefix icu4c)/lib/pkgconfig
|
||||||
# brew formulae are keg-only and not symlinked into HOMEBREW_PREFIX, force their usage
|
# macOS already has bison and flex in the system, but they are old and result in postgres-v14 target failure
|
||||||
EXTRA_PATH_OVERRIDES += $(shell brew --prefix bison)/bin/:$(shell brew --prefix flex)/bin/:
|
# brew formulae are keg-only and not symlinked into HOMEBREW_PREFIX, force their usage
|
||||||
|
EXTRA_PATH_OVERRIDES += $(shell brew --prefix bison)/bin/:$(shell brew --prefix flex)/bin/:
|
||||||
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
# Use -C option so that when PostgreSQL "make install" installs the
|
# Use -C option so that when PostgreSQL "make install" installs the
|
||||||
@@ -79,11 +94,14 @@ $(POSTGRES_INSTALL_DIR)/build/%/config.status:
|
|||||||
echo "'git submodule update --init --recursive --depth 2 --progress .' in project root.\n"; \
|
echo "'git submodule update --init --recursive --depth 2 --progress .' in project root.\n"; \
|
||||||
exit 1; }
|
exit 1; }
|
||||||
mkdir -p $(POSTGRES_INSTALL_DIR)/build/$*
|
mkdir -p $(POSTGRES_INSTALL_DIR)/build/$*
|
||||||
(cd $(POSTGRES_INSTALL_DIR)/build/$* && \
|
|
||||||
env PATH="$(EXTRA_PATH_OVERRIDES):$$PATH" $(ROOT_PROJECT_DIR)/vendor/postgres-$*/configure \
|
VERSION=$*; \
|
||||||
|
EXTRA_VERSION=$$(cd $(ROOT_PROJECT_DIR)/vendor/postgres-$$VERSION && git rev-parse HEAD); \
|
||||||
|
(cd $(POSTGRES_INSTALL_DIR)/build/$$VERSION && \
|
||||||
|
env PATH="$(EXTRA_PATH_OVERRIDES):$$PATH" $(ROOT_PROJECT_DIR)/vendor/postgres-$$VERSION/configure \
|
||||||
CFLAGS='$(PG_CFLAGS)' \
|
CFLAGS='$(PG_CFLAGS)' \
|
||||||
$(PG_CONFIGURE_OPTS) \
|
$(PG_CONFIGURE_OPTS) --with-extra-version=" ($$EXTRA_VERSION)" \
|
||||||
--prefix=$(abspath $(POSTGRES_INSTALL_DIR))/$* > configure.log)
|
--prefix=$(abspath $(POSTGRES_INSTALL_DIR))/$$VERSION > configure.log)
|
||||||
|
|
||||||
# nicer alias to run 'configure'
|
# nicer alias to run 'configure'
|
||||||
# Note: I've been unable to use templates for this part of our configuration.
|
# Note: I've been unable to use templates for this part of our configuration.
|
||||||
@@ -119,6 +137,8 @@ postgres-%: postgres-configure-% \
|
|||||||
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pageinspect install
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/pageinspect install
|
||||||
+@echo "Compiling amcheck $*"
|
+@echo "Compiling amcheck $*"
|
||||||
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/amcheck install
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/amcheck install
|
||||||
|
+@echo "Compiling test_decoding $*"
|
||||||
|
$(MAKE) -C $(POSTGRES_INSTALL_DIR)/build/$*/contrib/test_decoding install
|
||||||
|
|
||||||
.PHONY: postgres-clean-%
|
.PHONY: postgres-clean-%
|
||||||
postgres-clean-%:
|
postgres-clean-%:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
[](https://neon.tech)
|
[](https://neon.tech)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Neon
|
# Neon
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,12 @@ reqwest = { workspace = true, features = ["json"] }
|
|||||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||||
tokio-postgres.workspace = true
|
tokio-postgres.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
|
tokio-stream.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-opentelemetry.workspace = true
|
tracing-opentelemetry.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
tracing-utils.workspace = true
|
tracing-utils.workspace = true
|
||||||
|
thiserror.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
|
||||||
compute_api.workspace = true
|
compute_api.workspace = true
|
||||||
|
|||||||
@@ -47,10 +47,11 @@ use chrono::Utc;
|
|||||||
use clap::Arg;
|
use clap::Arg;
|
||||||
use signal_hook::consts::{SIGQUIT, SIGTERM};
|
use signal_hook::consts::{SIGQUIT, SIGTERM};
|
||||||
use signal_hook::{consts::SIGINT, iterator::Signals};
|
use signal_hook::{consts::SIGINT, iterator::Signals};
|
||||||
use tracing::{error, info};
|
use tracing::{error, info, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use compute_api::responses::ComputeStatus;
|
use compute_api::responses::ComputeStatus;
|
||||||
|
use compute_api::spec::ComputeSpec;
|
||||||
|
|
||||||
use compute_tools::compute::{
|
use compute_tools::compute::{
|
||||||
forward_termination_signal, ComputeNode, ComputeState, ParsedSpec, PG_PID,
|
forward_termination_signal, ComputeNode, ComputeState, ParsedSpec, PG_PID,
|
||||||
@@ -62,12 +63,41 @@ use compute_tools::logger::*;
|
|||||||
use compute_tools::monitor::launch_monitor;
|
use compute_tools::monitor::launch_monitor;
|
||||||
use compute_tools::params::*;
|
use compute_tools::params::*;
|
||||||
use compute_tools::spec::*;
|
use compute_tools::spec::*;
|
||||||
|
use compute_tools::swap::resize_swap;
|
||||||
|
|
||||||
// this is an arbitrary build tag. Fine as a default / for testing purposes
|
// this is an arbitrary build tag. Fine as a default / for testing purposes
|
||||||
// in-case of not-set environment var
|
// in-case of not-set environment var
|
||||||
const BUILD_TAG_DEFAULT: &str = "latest";
|
const BUILD_TAG_DEFAULT: &str = "latest";
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
|
let (build_tag, clap_args) = init()?;
|
||||||
|
|
||||||
|
let (pg_handle, start_pg_result) = {
|
||||||
|
// Enter startup tracing context
|
||||||
|
let _startup_context_guard = startup_context_from_env();
|
||||||
|
|
||||||
|
let cli_args = process_cli(&clap_args)?;
|
||||||
|
|
||||||
|
let cli_spec = try_spec_from_cli(&clap_args, &cli_args)?;
|
||||||
|
|
||||||
|
let wait_spec_result = wait_spec(build_tag, cli_args, cli_spec)?;
|
||||||
|
|
||||||
|
start_postgres(&clap_args, wait_spec_result)?
|
||||||
|
|
||||||
|
// Startup is finished, exit the startup tracing span
|
||||||
|
};
|
||||||
|
|
||||||
|
// PostgreSQL is now running, if startup was successful. Wait until it exits.
|
||||||
|
let wait_pg_result = wait_postgres(pg_handle)?;
|
||||||
|
|
||||||
|
let delay_exit = cleanup_after_postgres_exit(start_pg_result)?;
|
||||||
|
|
||||||
|
maybe_delay_exit(delay_exit);
|
||||||
|
|
||||||
|
deinit_and_exit(wait_pg_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init() -> Result<(String, clap::ArgMatches)> {
|
||||||
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
|
init_tracing_and_logging(DEFAULT_LOG_LEVEL)?;
|
||||||
|
|
||||||
let mut signals = Signals::new([SIGINT, SIGTERM, SIGQUIT])?;
|
let mut signals = Signals::new([SIGINT, SIGTERM, SIGQUIT])?;
|
||||||
@@ -82,9 +112,15 @@ fn main() -> Result<()> {
|
|||||||
.to_string();
|
.to_string();
|
||||||
info!("build_tag: {build_tag}");
|
info!("build_tag: {build_tag}");
|
||||||
|
|
||||||
let matches = cli().get_matches();
|
Ok((build_tag, cli().get_matches()))
|
||||||
let pgbin_default = String::from("postgres");
|
}
|
||||||
let pgbin = matches.get_one::<String>("pgbin").unwrap_or(&pgbin_default);
|
|
||||||
|
fn process_cli(matches: &clap::ArgMatches) -> Result<ProcessCliResult> {
|
||||||
|
let pgbin_default = "postgres";
|
||||||
|
let pgbin = matches
|
||||||
|
.get_one::<String>("pgbin")
|
||||||
|
.map(|s| s.as_str())
|
||||||
|
.unwrap_or(pgbin_default);
|
||||||
|
|
||||||
let ext_remote_storage = matches
|
let ext_remote_storage = matches
|
||||||
.get_one::<String>("remote-ext-config")
|
.get_one::<String>("remote-ext-config")
|
||||||
@@ -110,7 +146,32 @@ fn main() -> Result<()> {
|
|||||||
.expect("Postgres connection string is required");
|
.expect("Postgres connection string is required");
|
||||||
let spec_json = matches.get_one::<String>("spec");
|
let spec_json = matches.get_one::<String>("spec");
|
||||||
let spec_path = matches.get_one::<String>("spec-path");
|
let spec_path = matches.get_one::<String>("spec-path");
|
||||||
|
let resize_swap_on_bind = matches.get_flag("resize-swap-on-bind");
|
||||||
|
|
||||||
|
Ok(ProcessCliResult {
|
||||||
|
connstr,
|
||||||
|
pgdata,
|
||||||
|
pgbin,
|
||||||
|
ext_remote_storage,
|
||||||
|
http_port,
|
||||||
|
spec_json,
|
||||||
|
spec_path,
|
||||||
|
resize_swap_on_bind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ProcessCliResult<'clap> {
|
||||||
|
connstr: &'clap str,
|
||||||
|
pgdata: &'clap str,
|
||||||
|
pgbin: &'clap str,
|
||||||
|
ext_remote_storage: Option<&'clap str>,
|
||||||
|
http_port: u16,
|
||||||
|
spec_json: Option<&'clap String>,
|
||||||
|
spec_path: Option<&'clap String>,
|
||||||
|
resize_swap_on_bind: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn startup_context_from_env() -> Option<opentelemetry::ContextGuard> {
|
||||||
// Extract OpenTelemetry context for the startup actions from the
|
// Extract OpenTelemetry context for the startup actions from the
|
||||||
// TRACEPARENT and TRACESTATE env variables, and attach it to the current
|
// TRACEPARENT and TRACESTATE env variables, and attach it to the current
|
||||||
// tracing context.
|
// tracing context.
|
||||||
@@ -147,7 +208,7 @@ fn main() -> Result<()> {
|
|||||||
if let Ok(val) = std::env::var("TRACESTATE") {
|
if let Ok(val) = std::env::var("TRACESTATE") {
|
||||||
startup_tracing_carrier.insert("tracestate".to_string(), val);
|
startup_tracing_carrier.insert("tracestate".to_string(), val);
|
||||||
}
|
}
|
||||||
let startup_context_guard = if !startup_tracing_carrier.is_empty() {
|
if !startup_tracing_carrier.is_empty() {
|
||||||
use opentelemetry::propagation::TextMapPropagator;
|
use opentelemetry::propagation::TextMapPropagator;
|
||||||
use opentelemetry::sdk::propagation::TraceContextPropagator;
|
use opentelemetry::sdk::propagation::TraceContextPropagator;
|
||||||
let guard = TraceContextPropagator::new()
|
let guard = TraceContextPropagator::new()
|
||||||
@@ -157,8 +218,17 @@ fn main() -> Result<()> {
|
|||||||
Some(guard)
|
Some(guard)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_spec_from_cli(
|
||||||
|
matches: &clap::ArgMatches,
|
||||||
|
ProcessCliResult {
|
||||||
|
spec_json,
|
||||||
|
spec_path,
|
||||||
|
..
|
||||||
|
}: &ProcessCliResult,
|
||||||
|
) -> Result<CliSpecParams> {
|
||||||
let compute_id = matches.get_one::<String>("compute-id");
|
let compute_id = matches.get_one::<String>("compute-id");
|
||||||
let control_plane_uri = matches.get_one::<String>("control-plane-uri");
|
let control_plane_uri = matches.get_one::<String>("control-plane-uri");
|
||||||
|
|
||||||
@@ -199,6 +269,34 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Ok(CliSpecParams {
|
||||||
|
spec,
|
||||||
|
live_config_allowed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CliSpecParams {
|
||||||
|
/// If a spec was provided via CLI or file, the [`ComputeSpec`]
|
||||||
|
spec: Option<ComputeSpec>,
|
||||||
|
live_config_allowed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_spec(
|
||||||
|
build_tag: String,
|
||||||
|
ProcessCliResult {
|
||||||
|
connstr,
|
||||||
|
pgdata,
|
||||||
|
pgbin,
|
||||||
|
ext_remote_storage,
|
||||||
|
resize_swap_on_bind,
|
||||||
|
http_port,
|
||||||
|
..
|
||||||
|
}: ProcessCliResult,
|
||||||
|
CliSpecParams {
|
||||||
|
spec,
|
||||||
|
live_config_allowed,
|
||||||
|
}: CliSpecParams,
|
||||||
|
) -> Result<WaitSpecResult> {
|
||||||
let mut new_state = ComputeState::new();
|
let mut new_state = ComputeState::new();
|
||||||
let spec_set;
|
let spec_set;
|
||||||
|
|
||||||
@@ -226,19 +324,17 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
// If this is a pooled VM, prewarm before starting HTTP server and becoming
|
// If this is a pooled VM, prewarm before starting HTTP server and becoming
|
||||||
// available for binding. Prewarming helps Postgres start quicker later,
|
// available for binding. Prewarming helps Postgres start quicker later,
|
||||||
// because QEMU will already have it's memory allocated from the host, and
|
// because QEMU will already have its memory allocated from the host, and
|
||||||
// the necessary binaries will already be cached.
|
// the necessary binaries will already be cached.
|
||||||
if !spec_set {
|
if !spec_set {
|
||||||
compute.prewarm_postgres()?;
|
compute.prewarm_postgres()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Launch http service first, so we were able to serve control-plane
|
// Launch http service first, so that we can serve control-plane requests
|
||||||
// requests, while configuration is still in progress.
|
// while configuration is still in progress.
|
||||||
let _http_handle =
|
let _http_handle =
|
||||||
launch_http_server(http_port, &compute).expect("cannot launch http endpoint thread");
|
launch_http_server(http_port, &compute).expect("cannot launch http endpoint thread");
|
||||||
|
|
||||||
let extension_server_port: u16 = http_port;
|
|
||||||
|
|
||||||
if !spec_set {
|
if !spec_set {
|
||||||
// No spec provided, hang waiting for it.
|
// No spec provided, hang waiting for it.
|
||||||
info!("no compute spec provided, waiting");
|
info!("no compute spec provided, waiting");
|
||||||
@@ -253,21 +349,45 @@ fn main() -> Result<()> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record for how long we slept waiting for the spec.
|
||||||
|
let now = Utc::now();
|
||||||
|
state.metrics.wait_for_spec_ms = now
|
||||||
|
.signed_duration_since(state.start_time)
|
||||||
|
.to_std()
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as u64;
|
||||||
|
|
||||||
|
// Reset start time, so that the total startup time that is calculated later will
|
||||||
|
// not include the time that we waited for the spec.
|
||||||
|
state.start_time = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(WaitSpecResult {
|
||||||
|
compute,
|
||||||
|
http_port,
|
||||||
|
resize_swap_on_bind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WaitSpecResult {
|
||||||
|
compute: Arc<ComputeNode>,
|
||||||
|
// passed through from ProcessCliResult
|
||||||
|
http_port: u16,
|
||||||
|
resize_swap_on_bind: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_postgres(
|
||||||
|
// need to allow unused because `matches` is only used if target_os = "linux"
|
||||||
|
#[allow(unused_variables)] matches: &clap::ArgMatches,
|
||||||
|
WaitSpecResult {
|
||||||
|
compute,
|
||||||
|
http_port,
|
||||||
|
resize_swap_on_bind,
|
||||||
|
}: WaitSpecResult,
|
||||||
|
) -> Result<(Option<PostgresHandle>, StartPostgresResult)> {
|
||||||
// We got all we need, update the state.
|
// We got all we need, update the state.
|
||||||
let mut state = compute.state.lock().unwrap();
|
let mut state = compute.state.lock().unwrap();
|
||||||
|
|
||||||
// Record for how long we slept waiting for the spec.
|
|
||||||
state.metrics.wait_for_spec_ms = Utc::now()
|
|
||||||
.signed_duration_since(state.start_time)
|
|
||||||
.to_std()
|
|
||||||
.unwrap()
|
|
||||||
.as_millis() as u64;
|
|
||||||
// Reset start time to the actual start of the configuration, so that
|
|
||||||
// total startup time was properly measured at the end.
|
|
||||||
state.start_time = Utc::now();
|
|
||||||
|
|
||||||
state.status = ComputeStatus::Init;
|
state.status = ComputeStatus::Init;
|
||||||
compute.state_changed.notify_all();
|
compute.state_changed.notify_all();
|
||||||
|
|
||||||
@@ -275,33 +395,72 @@ fn main() -> Result<()> {
|
|||||||
"running compute with features: {:?}",
|
"running compute with features: {:?}",
|
||||||
state.pspec.as_ref().unwrap().spec.features
|
state.pspec.as_ref().unwrap().spec.features
|
||||||
);
|
);
|
||||||
|
// before we release the mutex, fetch the swap size (if any) for later.
|
||||||
|
let swap_size_bytes = state.pspec.as_ref().unwrap().spec.swap_size_bytes;
|
||||||
drop(state);
|
drop(state);
|
||||||
|
|
||||||
// Launch remaining service threads
|
// Launch remaining service threads
|
||||||
let _monitor_handle = launch_monitor(&compute);
|
let _monitor_handle = launch_monitor(&compute);
|
||||||
let _configurator_handle = launch_configurator(&compute);
|
let _configurator_handle = launch_configurator(&compute);
|
||||||
|
|
||||||
// Start Postgres
|
let mut prestartup_failed = false;
|
||||||
let mut delay_exit = false;
|
let mut delay_exit = false;
|
||||||
let mut exit_code = None;
|
|
||||||
let pg = match compute.start_compute(extension_server_port) {
|
// Resize swap to the desired size if the compute spec says so
|
||||||
Ok(pg) => Some(pg),
|
if let (Some(size_bytes), true) = (swap_size_bytes, resize_swap_on_bind) {
|
||||||
Err(err) => {
|
// To avoid 'swapoff' hitting postgres startup, we need to run resize-swap to completion
|
||||||
error!("could not start the compute node: {:#}", err);
|
// *before* starting postgres.
|
||||||
let mut state = compute.state.lock().unwrap();
|
//
|
||||||
state.error = Some(format!("{:?}", err));
|
// In theory, we could do this asynchronously if SkipSwapon was enabled for VMs, but this
|
||||||
state.status = ComputeStatus::Failed;
|
// carries a risk of introducing hard-to-debug issues - e.g. if postgres sometimes gets
|
||||||
// Notify others that Postgres failed to start. In case of configuring the
|
// OOM-killed during startup because swap wasn't available yet.
|
||||||
// empty compute, it's likely that API handler is still waiting for compute
|
match resize_swap(size_bytes) {
|
||||||
// state change. With this we will notify it that compute is in Failed state,
|
Ok(()) => {
|
||||||
// so control plane will know about it earlier and record proper error instead
|
let size_gib = size_bytes as f32 / (1 << 20) as f32; // just for more coherent display.
|
||||||
// of timeout.
|
info!(%size_bytes, %size_gib, "resized swap");
|
||||||
compute.state_changed.notify_all();
|
}
|
||||||
drop(state); // unlock
|
Err(err) => {
|
||||||
delay_exit = true;
|
let err = err.context("failed to resize swap");
|
||||||
None
|
error!("{err:#}");
|
||||||
|
|
||||||
|
// Mark compute startup as failed; don't try to start postgres, and report this
|
||||||
|
// error to the control plane when it next asks.
|
||||||
|
prestartup_failed = true;
|
||||||
|
let mut state = compute.state.lock().unwrap();
|
||||||
|
state.error = Some(format!("{err:?}"));
|
||||||
|
state.status = ComputeStatus::Failed;
|
||||||
|
compute.state_changed.notify_all();
|
||||||
|
delay_exit = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
let extension_server_port: u16 = http_port;
|
||||||
|
|
||||||
|
// Start Postgres
|
||||||
|
let mut pg = None;
|
||||||
|
if !prestartup_failed {
|
||||||
|
pg = match compute.start_compute(extension_server_port) {
|
||||||
|
Ok(pg) => Some(pg),
|
||||||
|
Err(err) => {
|
||||||
|
error!("could not start the compute node: {:#}", err);
|
||||||
|
let mut state = compute.state.lock().unwrap();
|
||||||
|
state.error = Some(format!("{:?}", err));
|
||||||
|
state.status = ComputeStatus::Failed;
|
||||||
|
// Notify others that Postgres failed to start. In case of configuring the
|
||||||
|
// empty compute, it's likely that API handler is still waiting for compute
|
||||||
|
// state change. With this we will notify it that compute is in Failed state,
|
||||||
|
// so control plane will know about it earlier and record proper error instead
|
||||||
|
// of timeout.
|
||||||
|
compute.state_changed.notify_all();
|
||||||
|
drop(state); // unlock
|
||||||
|
delay_exit = true;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
warn!("skipping postgres startup because pre-startup step failed");
|
||||||
|
}
|
||||||
|
|
||||||
// Start the vm-monitor if directed to. The vm-monitor only runs on linux
|
// Start the vm-monitor if directed to. The vm-monitor only runs on linux
|
||||||
// because it requires cgroups.
|
// because it requires cgroups.
|
||||||
@@ -334,7 +493,7 @@ fn main() -> Result<()> {
|
|||||||
// This token is used internally by the monitor to clean up all threads
|
// This token is used internally by the monitor to clean up all threads
|
||||||
let token = CancellationToken::new();
|
let token = CancellationToken::new();
|
||||||
|
|
||||||
let vm_monitor = &rt.as_ref().map(|rt| {
|
let vm_monitor = rt.as_ref().map(|rt| {
|
||||||
rt.spawn(vm_monitor::start(
|
rt.spawn(vm_monitor::start(
|
||||||
Box::leak(Box::new(vm_monitor::Args {
|
Box::leak(Box::new(vm_monitor::Args {
|
||||||
cgroup: cgroup.cloned(),
|
cgroup: cgroup.cloned(),
|
||||||
@@ -347,12 +506,41 @@ fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
pg,
|
||||||
|
StartPostgresResult {
|
||||||
|
delay_exit,
|
||||||
|
compute,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
rt,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
token,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
vm_monitor,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostgresHandle = (std::process::Child, std::thread::JoinHandle<()>);
|
||||||
|
|
||||||
|
struct StartPostgresResult {
|
||||||
|
delay_exit: bool,
|
||||||
|
// passed through from WaitSpecResult
|
||||||
|
compute: Arc<ComputeNode>,
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
rt: Option<tokio::runtime::Runtime>,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
token: tokio_util::sync::CancellationToken,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
vm_monitor: Option<tokio::task::JoinHandle<Result<()>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait_postgres(pg: Option<PostgresHandle>) -> Result<WaitPostgresResult> {
|
||||||
// Wait for the child Postgres process forever. In this state Ctrl+C will
|
// Wait for the child Postgres process forever. In this state Ctrl+C will
|
||||||
// propagate to Postgres and it will be shut down as well.
|
// propagate to Postgres and it will be shut down as well.
|
||||||
|
let mut exit_code = None;
|
||||||
if let Some((mut pg, logs_handle)) = pg {
|
if let Some((mut pg, logs_handle)) = pg {
|
||||||
// Startup is finished, exit the startup tracing span
|
|
||||||
drop(startup_context_guard);
|
|
||||||
|
|
||||||
let ecode = pg
|
let ecode = pg
|
||||||
.wait()
|
.wait()
|
||||||
.expect("failed to start waiting on Postgres process");
|
.expect("failed to start waiting on Postgres process");
|
||||||
@@ -367,6 +555,25 @@ fn main() -> Result<()> {
|
|||||||
exit_code = ecode.code()
|
exit_code = ecode.code()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(WaitPostgresResult { exit_code })
|
||||||
|
}
|
||||||
|
|
||||||
|
struct WaitPostgresResult {
|
||||||
|
exit_code: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup_after_postgres_exit(
|
||||||
|
StartPostgresResult {
|
||||||
|
mut delay_exit,
|
||||||
|
compute,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
vm_monitor,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
token,
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
rt,
|
||||||
|
}: StartPostgresResult,
|
||||||
|
) -> Result<bool> {
|
||||||
// Terminate the vm_monitor so it releases the file watcher on
|
// Terminate the vm_monitor so it releases the file watcher on
|
||||||
// /sys/fs/cgroup/neon-postgres.
|
// /sys/fs/cgroup/neon-postgres.
|
||||||
// Note: the vm-monitor only runs on linux because it requires cgroups.
|
// Note: the vm-monitor only runs on linux because it requires cgroups.
|
||||||
@@ -408,13 +615,19 @@ fn main() -> Result<()> {
|
|||||||
error!("error while checking for core dumps: {err:?}");
|
error!("error while checking for core dumps: {err:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(delay_exit)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_delay_exit(delay_exit: bool) {
|
||||||
// If launch failed, keep serving HTTP requests for a while, so the cloud
|
// If launch failed, keep serving HTTP requests for a while, so the cloud
|
||||||
// control plane can get the actual error.
|
// control plane can get the actual error.
|
||||||
if delay_exit {
|
if delay_exit {
|
||||||
info!("giving control plane 30s to collect the error before shutdown");
|
info!("giving control plane 30s to collect the error before shutdown");
|
||||||
thread::sleep(Duration::from_secs(30));
|
thread::sleep(Duration::from_secs(30));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deinit_and_exit(WaitPostgresResult { exit_code }: WaitPostgresResult) -> ! {
|
||||||
// Shutdown trace pipeline gracefully, so that it has a chance to send any
|
// Shutdown trace pipeline gracefully, so that it has a chance to send any
|
||||||
// pending traces before we exit. Shutting down OTEL tracing provider may
|
// pending traces before we exit. Shutting down OTEL tracing provider may
|
||||||
// hang for quite some time, see, for example:
|
// hang for quite some time, see, for example:
|
||||||
@@ -522,10 +735,15 @@ fn cli() -> clap::Command {
|
|||||||
Arg::new("filecache-connstr")
|
Arg::new("filecache-connstr")
|
||||||
.long("filecache-connstr")
|
.long("filecache-connstr")
|
||||||
.default_value(
|
.default_value(
|
||||||
"host=localhost port=5432 dbname=postgres user=cloud_admin sslmode=disable",
|
"host=localhost port=5432 dbname=postgres user=cloud_admin sslmode=disable application_name=vm-monitor",
|
||||||
)
|
)
|
||||||
.value_name("FILECACHE_CONNSTR"),
|
.value_name("FILECACHE_CONNSTR"),
|
||||||
)
|
)
|
||||||
|
.arg(
|
||||||
|
Arg::new("resize-swap-on-bind")
|
||||||
|
.long("resize-swap-on-bind")
|
||||||
|
.action(clap::ArgAction::SetTrue),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// When compute_ctl is killed, send also termination signal to sync-safekeepers
|
/// When compute_ctl is killed, send also termination signal to sync-safekeepers
|
||||||
|
|||||||
116
compute_tools/src/catalog.rs
Normal file
116
compute_tools/src/catalog.rs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
use compute_api::{
|
||||||
|
responses::CatalogObjects,
|
||||||
|
spec::{Database, Role},
|
||||||
|
};
|
||||||
|
use futures::Stream;
|
||||||
|
use postgres::{Client, NoTls};
|
||||||
|
use std::{path::Path, process::Stdio, result::Result, sync::Arc};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncBufReadExt, BufReader},
|
||||||
|
process::Command,
|
||||||
|
task,
|
||||||
|
};
|
||||||
|
use tokio_stream::{self as stream, StreamExt};
|
||||||
|
use tokio_util::codec::{BytesCodec, FramedRead};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
compute::ComputeNode,
|
||||||
|
pg_helpers::{get_existing_dbs, get_existing_roles},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub async fn get_dbs_and_roles(compute: &Arc<ComputeNode>) -> anyhow::Result<CatalogObjects> {
|
||||||
|
let connstr = compute.connstr.clone();
|
||||||
|
task::spawn_blocking(move || {
|
||||||
|
let mut client = Client::connect(connstr.as_str(), NoTls)?;
|
||||||
|
let roles: Vec<Role>;
|
||||||
|
{
|
||||||
|
let mut xact = client.transaction()?;
|
||||||
|
roles = get_existing_roles(&mut xact)?;
|
||||||
|
}
|
||||||
|
let databases: Vec<Database> = get_existing_dbs(&mut client)?.values().cloned().collect();
|
||||||
|
|
||||||
|
Ok(CatalogObjects { roles, databases })
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum SchemaDumpError {
|
||||||
|
#[error("Database does not exist.")]
|
||||||
|
DatabaseDoesNotExist,
|
||||||
|
#[error("Failed to execute pg_dump.")]
|
||||||
|
IO(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
// It uses the pg_dump utility to dump the schema of the specified database.
|
||||||
|
// The output is streamed back to the caller and supposed to be streamed via HTTP.
|
||||||
|
//
|
||||||
|
// Before return the result with the output, it checks that pg_dump produced any output.
|
||||||
|
// If not, it tries to parse the stderr output to determine if the database does not exist
|
||||||
|
// and special error is returned.
|
||||||
|
//
|
||||||
|
// To make sure that the process is killed when the caller drops the stream, we use tokio kill_on_drop feature.
|
||||||
|
pub async fn get_database_schema(
|
||||||
|
compute: &Arc<ComputeNode>,
|
||||||
|
dbname: &str,
|
||||||
|
) -> Result<impl Stream<Item = Result<bytes::Bytes, std::io::Error>>, SchemaDumpError> {
|
||||||
|
let pgbin = &compute.pgbin;
|
||||||
|
let basepath = Path::new(pgbin).parent().unwrap();
|
||||||
|
let pgdump = basepath.join("pg_dump");
|
||||||
|
let mut connstr = compute.connstr.clone();
|
||||||
|
connstr.set_path(dbname);
|
||||||
|
let mut cmd = Command::new(pgdump)
|
||||||
|
.arg("--schema-only")
|
||||||
|
.arg(connstr.as_str())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.kill_on_drop(true)
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
let stdout = cmd.stdout.take().ok_or_else(|| {
|
||||||
|
std::io::Error::new(std::io::ErrorKind::Other, "Failed to capture stdout.")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let stderr = cmd.stderr.take().ok_or_else(|| {
|
||||||
|
std::io::Error::new(std::io::ErrorKind::Other, "Failed to capture stderr.")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let mut stdout_reader = FramedRead::new(stdout, BytesCodec::new());
|
||||||
|
let stderr_reader = BufReader::new(stderr);
|
||||||
|
|
||||||
|
let first_chunk = match stdout_reader.next().await {
|
||||||
|
Some(Ok(bytes)) if !bytes.is_empty() => bytes,
|
||||||
|
Some(Err(e)) => {
|
||||||
|
return Err(SchemaDumpError::IO(e));
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let mut lines = stderr_reader.lines();
|
||||||
|
if let Some(line) = lines.next_line().await? {
|
||||||
|
if line.contains(&format!("FATAL: database \"{}\" does not exist", dbname)) {
|
||||||
|
return Err(SchemaDumpError::DatabaseDoesNotExist);
|
||||||
|
}
|
||||||
|
warn!("pg_dump stderr: {}", line)
|
||||||
|
}
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Ok(Some(line)) = lines.next_line().await {
|
||||||
|
warn!("pg_dump stderr: {}", line)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Err(SchemaDumpError::IO(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"failed to start pg_dump",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let initial_stream = stream::once(Ok(first_chunk.freeze()));
|
||||||
|
// Consume stderr and log warnings
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut lines = stderr_reader.lines();
|
||||||
|
while let Ok(Some(line)) = lines.next_line().await {
|
||||||
|
warn!("pg_dump stderr: {}", line)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Ok(initial_stream.chain(stdout_reader.map(|res| res.map(|b| b.freeze()))))
|
||||||
|
}
|
||||||
@@ -818,9 +818,15 @@ impl ComputeNode {
|
|||||||
Client::connect(zenith_admin_connstr.as_str(), NoTls)
|
Client::connect(zenith_admin_connstr.as_str(), NoTls)
|
||||||
.context("broken cloud_admin credential: tried connecting with cloud_admin but could not authenticate, and zenith_admin does not work either")?;
|
.context("broken cloud_admin credential: tried connecting with cloud_admin but could not authenticate, and zenith_admin does not work either")?;
|
||||||
// Disable forwarding so that users don't get a cloud_admin role
|
// Disable forwarding so that users don't get a cloud_admin role
|
||||||
client.simple_query("SET neon.forward_ddl = false")?;
|
|
||||||
client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?;
|
let mut func = || {
|
||||||
client.simple_query("GRANT zenith_admin TO cloud_admin")?;
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
|
client.simple_query("CREATE USER cloud_admin WITH SUPERUSER")?;
|
||||||
|
client.simple_query("GRANT zenith_admin TO cloud_admin")?;
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
};
|
||||||
|
func().context("apply_config setup cloud_admin")?;
|
||||||
|
|
||||||
drop(client);
|
drop(client);
|
||||||
|
|
||||||
// reconnect with connstring with expected name
|
// reconnect with connstring with expected name
|
||||||
@@ -832,24 +838,29 @@ impl ComputeNode {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
||||||
client.simple_query("SET neon.forward_ddl = false")?;
|
client
|
||||||
|
.simple_query("SET neon.forward_ddl = false")
|
||||||
|
.context("apply_config SET neon.forward_ddl = false")?;
|
||||||
|
|
||||||
// Proceed with post-startup configuration. Note, that order of operations is important.
|
// Proceed with post-startup configuration. Note, that order of operations is important.
|
||||||
let spec = &compute_state.pspec.as_ref().expect("spec must be set").spec;
|
let spec = &compute_state.pspec.as_ref().expect("spec must be set").spec;
|
||||||
create_neon_superuser(spec, &mut client)?;
|
create_neon_superuser(spec, &mut client).context("apply_config create_neon_superuser")?;
|
||||||
cleanup_instance(&mut client)?;
|
cleanup_instance(&mut client).context("apply_config cleanup_instance")?;
|
||||||
handle_roles(spec, &mut client)?;
|
handle_roles(spec, &mut client).context("apply_config handle_roles")?;
|
||||||
handle_databases(spec, &mut client)?;
|
handle_databases(spec, &mut client).context("apply_config handle_databases")?;
|
||||||
handle_role_deletions(spec, connstr.as_str(), &mut client)?;
|
handle_role_deletions(spec, connstr.as_str(), &mut client)
|
||||||
|
.context("apply_config handle_role_deletions")?;
|
||||||
handle_grants(
|
handle_grants(
|
||||||
spec,
|
spec,
|
||||||
&mut client,
|
&mut client,
|
||||||
connstr.as_str(),
|
connstr.as_str(),
|
||||||
self.has_feature(ComputeFeature::AnonExtension),
|
self.has_feature(ComputeFeature::AnonExtension),
|
||||||
)?;
|
)
|
||||||
handle_extensions(spec, &mut client)?;
|
.context("apply_config handle_grants")?;
|
||||||
handle_extension_neon(&mut client)?;
|
handle_extensions(spec, &mut client).context("apply_config handle_extensions")?;
|
||||||
create_availability_check_data(&mut client)?;
|
handle_extension_neon(&mut client).context("apply_config handle_extension_neon")?;
|
||||||
|
create_availability_check_data(&mut client)
|
||||||
|
.context("apply_config create_availability_check_data")?;
|
||||||
|
|
||||||
// 'Close' connection
|
// 'Close' connection
|
||||||
drop(client);
|
drop(client);
|
||||||
@@ -857,7 +868,7 @@ impl ComputeNode {
|
|||||||
// Run migrations separately to not hold up cold starts
|
// Run migrations separately to not hold up cold starts
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut client = Client::connect(connstr.as_str(), NoTls)?;
|
let mut client = Client::connect(connstr.as_str(), NoTls)?;
|
||||||
handle_migrations(&mut client)
|
handle_migrations(&mut client).context("apply_config handle_migrations")
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -907,38 +918,39 @@ impl ComputeNode {
|
|||||||
// temporarily reset max_cluster_size in config
|
// temporarily reset max_cluster_size in config
|
||||||
// to avoid the possibility of hitting the limit, while we are reconfiguring:
|
// to avoid the possibility of hitting the limit, while we are reconfiguring:
|
||||||
// creating new extensions, roles, etc...
|
// creating new extensions, roles, etc...
|
||||||
config::compute_ctl_temp_override_create(pgdata_path, "neon.max_cluster_size=-1")?;
|
config::with_compute_ctl_tmp_override(pgdata_path, "neon.max_cluster_size=-1", || {
|
||||||
self.pg_reload_conf()?;
|
self.pg_reload_conf()?;
|
||||||
|
|
||||||
let mut client = Client::connect(self.connstr.as_str(), NoTls)?;
|
let mut client = Client::connect(self.connstr.as_str(), NoTls)?;
|
||||||
|
|
||||||
// Proceed with post-startup configuration. Note, that order of operations is important.
|
// Proceed with post-startup configuration. Note, that order of operations is important.
|
||||||
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
// Disable DDL forwarding because control plane already knows about these roles/databases.
|
||||||
if spec.mode == ComputeMode::Primary {
|
if spec.mode == ComputeMode::Primary {
|
||||||
client.simple_query("SET neon.forward_ddl = false")?;
|
client.simple_query("SET neon.forward_ddl = false")?;
|
||||||
cleanup_instance(&mut client)?;
|
cleanup_instance(&mut client)?;
|
||||||
handle_roles(&spec, &mut client)?;
|
handle_roles(&spec, &mut client)?;
|
||||||
handle_databases(&spec, &mut client)?;
|
handle_databases(&spec, &mut client)?;
|
||||||
handle_role_deletions(&spec, self.connstr.as_str(), &mut client)?;
|
handle_role_deletions(&spec, self.connstr.as_str(), &mut client)?;
|
||||||
handle_grants(
|
handle_grants(
|
||||||
&spec,
|
&spec,
|
||||||
&mut client,
|
&mut client,
|
||||||
self.connstr.as_str(),
|
self.connstr.as_str(),
|
||||||
self.has_feature(ComputeFeature::AnonExtension),
|
self.has_feature(ComputeFeature::AnonExtension),
|
||||||
)?;
|
)?;
|
||||||
handle_extensions(&spec, &mut client)?;
|
handle_extensions(&spec, &mut client)?;
|
||||||
handle_extension_neon(&mut client)?;
|
handle_extension_neon(&mut client)?;
|
||||||
// We can skip handle_migrations here because a new migration can only appear
|
// We can skip handle_migrations here because a new migration can only appear
|
||||||
// if we have a new version of the compute_ctl binary, which can only happen
|
// if we have a new version of the compute_ctl binary, which can only happen
|
||||||
// if compute got restarted, in which case we'll end up inside of apply_config
|
// if compute got restarted, in which case we'll end up inside of apply_config
|
||||||
// instead of reconfigure.
|
// instead of reconfigure.
|
||||||
}
|
}
|
||||||
|
|
||||||
// 'Close' connection
|
// 'Close' connection
|
||||||
drop(client);
|
drop(client);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})?;
|
||||||
|
|
||||||
// reset max_cluster_size in config back to original value and reload config
|
|
||||||
config::compute_ctl_temp_override_remove(pgdata_path)?;
|
|
||||||
self.pg_reload_conf()?;
|
self.pg_reload_conf()?;
|
||||||
|
|
||||||
let unknown_op = "unknown".to_string();
|
let unknown_op = "unknown".to_string();
|
||||||
@@ -1029,12 +1041,17 @@ impl ComputeNode {
|
|||||||
// temporarily reset max_cluster_size in config
|
// temporarily reset max_cluster_size in config
|
||||||
// to avoid the possibility of hitting the limit, while we are applying config:
|
// to avoid the possibility of hitting the limit, while we are applying config:
|
||||||
// creating new extensions, roles, etc...
|
// creating new extensions, roles, etc...
|
||||||
config::compute_ctl_temp_override_create(pgdata_path, "neon.max_cluster_size=-1")?;
|
config::with_compute_ctl_tmp_override(
|
||||||
self.pg_reload_conf()?;
|
pgdata_path,
|
||||||
|
"neon.max_cluster_size=-1",
|
||||||
|
|| {
|
||||||
|
self.pg_reload_conf()?;
|
||||||
|
|
||||||
self.apply_config(&compute_state)?;
|
self.apply_config(&compute_state)?;
|
||||||
|
|
||||||
config::compute_ctl_temp_override_remove(pgdata_path)?;
|
Ok(())
|
||||||
|
},
|
||||||
|
)?;
|
||||||
self.pg_reload_conf()?;
|
self.pg_reload_conf()?;
|
||||||
}
|
}
|
||||||
self.post_apply_config()?;
|
self.post_apply_config()?;
|
||||||
@@ -1262,10 +1279,12 @@ LIMIT 100",
|
|||||||
.await
|
.await
|
||||||
.map_err(DownloadError::Other);
|
.map_err(DownloadError::Other);
|
||||||
|
|
||||||
self.ext_download_progress
|
if download_size.is_ok() {
|
||||||
.write()
|
self.ext_download_progress
|
||||||
.expect("bad lock")
|
.write()
|
||||||
.insert(ext_archive_name.to_string(), (download_start, true));
|
.expect("bad lock")
|
||||||
|
.insert(ext_archive_name.to_string(), (download_start, true));
|
||||||
|
}
|
||||||
|
|
||||||
download_size
|
download_size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use std::path::Path;
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::pg_helpers::escape_conf_value;
|
use crate::pg_helpers::escape_conf_value;
|
||||||
use crate::pg_helpers::PgOptionsSerialize;
|
use crate::pg_helpers::{GenericOptionExt, PgOptionsSerialize};
|
||||||
use compute_api::spec::{ComputeMode, ComputeSpec};
|
use compute_api::spec::{ComputeMode, ComputeSpec, GenericOption};
|
||||||
|
|
||||||
/// Check that `line` is inside a text file and put it there if it is not.
|
/// Check that `line` is inside a text file and put it there if it is not.
|
||||||
/// Create file if it doesn't exist.
|
/// Create file if it doesn't exist.
|
||||||
@@ -83,12 +83,27 @@ pub fn write_postgres_conf(
|
|||||||
ComputeMode::Replica => {
|
ComputeMode::Replica => {
|
||||||
// hot_standby is 'on' by default, but let's be explicit
|
// hot_standby is 'on' by default, but let's be explicit
|
||||||
writeln!(file, "hot_standby=on")?;
|
writeln!(file, "hot_standby=on")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Inform the replica about the primary state
|
if cfg!(target_os = "linux") {
|
||||||
// Default is 'false'
|
// Check /proc/sys/vm/overcommit_memory -- if it equals 2 (i.e. linux memory overcommit is
|
||||||
if let Some(primary_is_running) = spec.primary_is_running {
|
// disabled), then the control plane has enabled swap and we should set
|
||||||
writeln!(file, "neon.primary_is_running={}", primary_is_running)?;
|
// dynamic_shared_memory_type = 'mmap'.
|
||||||
}
|
//
|
||||||
|
// This is (maybe?) temporary - for more, see https://github.com/neondatabase/cloud/issues/12047.
|
||||||
|
let overcommit_memory_contents = std::fs::read_to_string("/proc/sys/vm/overcommit_memory")
|
||||||
|
// ignore any errors - they may be expected to occur under certain situations (e.g. when
|
||||||
|
// not running in Linux).
|
||||||
|
.unwrap_or_else(|_| String::new());
|
||||||
|
if overcommit_memory_contents.trim() == "2" {
|
||||||
|
let opt = GenericOption {
|
||||||
|
name: "dynamic_shared_memory_type".to_owned(),
|
||||||
|
value: Some("mmap".to_owned()),
|
||||||
|
vartype: "enum".to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(file, "{}", opt.to_pg_setting())?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,18 +125,17 @@ pub fn write_postgres_conf(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// create file compute_ctl_temp_override.conf in pgdata_dir
|
pub fn with_compute_ctl_tmp_override<F>(pgdata_path: &Path, options: &str, exec: F) -> Result<()>
|
||||||
/// add provided options to this file
|
where
|
||||||
pub fn compute_ctl_temp_override_create(pgdata_path: &Path, options: &str) -> Result<()> {
|
F: FnOnce() -> Result<()>,
|
||||||
|
{
|
||||||
let path = pgdata_path.join("compute_ctl_temp_override.conf");
|
let path = pgdata_path.join("compute_ctl_temp_override.conf");
|
||||||
let mut file = File::create(path)?;
|
let mut file = File::create(path)?;
|
||||||
write!(file, "{}", options)?;
|
write!(file, "{}", options)?;
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// remove file compute_ctl_temp_override.conf in pgdata_dir
|
let res = exec();
|
||||||
pub fn compute_ctl_temp_override_remove(pgdata_path: &Path) -> Result<()> {
|
|
||||||
let path = pgdata_path.join("compute_ctl_temp_override.conf");
|
file.set_len(0)?;
|
||||||
std::fs::remove_file(path)?;
|
|
||||||
Ok(())
|
res
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,17 +5,21 @@ use std::net::SocketAddr;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
use crate::catalog::SchemaDumpError;
|
||||||
|
use crate::catalog::{get_database_schema, get_dbs_and_roles};
|
||||||
use crate::compute::forward_termination_signal;
|
use crate::compute::forward_termination_signal;
|
||||||
use crate::compute::{ComputeNode, ComputeState, ParsedSpec};
|
use crate::compute::{ComputeNode, ComputeState, ParsedSpec};
|
||||||
use compute_api::requests::ConfigurationRequest;
|
use compute_api::requests::ConfigurationRequest;
|
||||||
use compute_api::responses::{ComputeStatus, ComputeStatusResponse, GenericAPIError};
|
use compute_api::responses::{ComputeStatus, ComputeStatusResponse, GenericAPIError};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use hyper::header::CONTENT_TYPE;
|
||||||
use hyper::service::{make_service_fn, service_fn};
|
use hyper::service::{make_service_fn, service_fn};
|
||||||
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
use hyper::{Body, Method, Request, Response, Server, StatusCode};
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
use tracing::{error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
use tracing_utils::http::OtelName;
|
use tracing_utils::http::OtelName;
|
||||||
|
use utils::http::request::must_get_query_param;
|
||||||
|
|
||||||
fn status_response_from_state(state: &ComputeState) -> ComputeStatusResponse {
|
fn status_response_from_state(state: &ComputeState) -> ComputeStatusResponse {
|
||||||
ComputeStatusResponse {
|
ComputeStatusResponse {
|
||||||
@@ -44,7 +48,7 @@ async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body
|
|||||||
match (req.method(), req.uri().path()) {
|
match (req.method(), req.uri().path()) {
|
||||||
// Serialized compute state.
|
// Serialized compute state.
|
||||||
(&Method::GET, "/status") => {
|
(&Method::GET, "/status") => {
|
||||||
info!("serving /status GET request");
|
debug!("serving /status GET request");
|
||||||
let state = compute.state.lock().unwrap();
|
let state = compute.state.lock().unwrap();
|
||||||
let status_response = status_response_from_state(&state);
|
let status_response = status_response_from_state(&state);
|
||||||
Response::new(Body::from(serde_json::to_string(&status_response).unwrap()))
|
Response::new(Body::from(serde_json::to_string(&status_response).unwrap()))
|
||||||
@@ -133,6 +137,34 @@ async fn routes(req: Request<Body>, compute: &Arc<ComputeNode>) -> Response<Body
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(&Method::GET, "/dbs_and_roles") => {
|
||||||
|
info!("serving /dbs_and_roles GET request",);
|
||||||
|
match get_dbs_and_roles(compute).await {
|
||||||
|
Ok(res) => render_json(Body::from(serde_json::to_string(&res).unwrap())),
|
||||||
|
Err(_) => {
|
||||||
|
render_json_error("can't get dbs and roles", StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(&Method::GET, "/database_schema") => {
|
||||||
|
let database = match must_get_query_param(&req, "database") {
|
||||||
|
Err(e) => return e.into_response(),
|
||||||
|
Ok(database) => database,
|
||||||
|
};
|
||||||
|
info!("serving /database_schema GET request with database: {database}",);
|
||||||
|
match get_database_schema(compute, &database).await {
|
||||||
|
Ok(res) => render_plain(Body::wrap_stream(res)),
|
||||||
|
Err(SchemaDumpError::DatabaseDoesNotExist) => {
|
||||||
|
render_json_error("database does not exist", StatusCode::NOT_FOUND)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("can't get schema dump: {}", e);
|
||||||
|
render_json_error("can't get schema dump", StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// download extension files from remote extension storage on demand
|
// download extension files from remote extension storage on demand
|
||||||
(&Method::POST, route) if route.starts_with("/extension_server/") => {
|
(&Method::POST, route) if route.starts_with("/extension_server/") => {
|
||||||
info!("serving {:?} POST request", route);
|
info!("serving {:?} POST request", route);
|
||||||
@@ -303,10 +335,25 @@ fn render_json_error(e: &str, status: StatusCode) -> Response<Body> {
|
|||||||
};
|
};
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(status)
|
.status(status)
|
||||||
|
.header(CONTENT_TYPE, "application/json")
|
||||||
.body(Body::from(serde_json::to_string(&error).unwrap()))
|
.body(Body::from(serde_json::to_string(&error).unwrap()))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_json(body: Body) -> Response<Body> {
|
||||||
|
Response::builder()
|
||||||
|
.header(CONTENT_TYPE, "application/json")
|
||||||
|
.body(body)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_plain(body: Body) -> Response<Body> {
|
||||||
|
Response::builder()
|
||||||
|
.header(CONTENT_TYPE, "text/plain")
|
||||||
|
.body(body)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_terminate_request(compute: &Arc<ComputeNode>) -> Result<(), (String, StatusCode)> {
|
async fn handle_terminate_request(compute: &Arc<ComputeNode>) -> Result<(), (String, StatusCode)> {
|
||||||
{
|
{
|
||||||
let mut state = compute.state.lock().unwrap();
|
let mut state = compute.state.lock().unwrap();
|
||||||
|
|||||||
@@ -68,6 +68,51 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Info"
|
$ref: "#/components/schemas/Info"
|
||||||
|
|
||||||
|
/dbs_and_roles:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Info
|
||||||
|
summary: Get databases and roles in the catalog.
|
||||||
|
description: ""
|
||||||
|
operationId: getDbsAndRoles
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Compute schema objects
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/DbsAndRoles"
|
||||||
|
|
||||||
|
/database_schema:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Info
|
||||||
|
summary: Get schema dump
|
||||||
|
parameters:
|
||||||
|
- name: database
|
||||||
|
in: query
|
||||||
|
description: Database name to dump.
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
example: "postgres"
|
||||||
|
description: Get schema dump in SQL format.
|
||||||
|
operationId: getDatabaseSchema
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Schema dump
|
||||||
|
content:
|
||||||
|
text/plain:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Schema dump in SQL format.
|
||||||
|
404:
|
||||||
|
description: Non existing database.
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: "#/components/schemas/GenericError"
|
||||||
|
|
||||||
/check_writability:
|
/check_writability:
|
||||||
post:
|
post:
|
||||||
tags:
|
tags:
|
||||||
@@ -229,6 +274,73 @@ components:
|
|||||||
num_cpus:
|
num_cpus:
|
||||||
type: integer
|
type: integer
|
||||||
|
|
||||||
|
DbsAndRoles:
|
||||||
|
type: object
|
||||||
|
description: Databases and Roles
|
||||||
|
required:
|
||||||
|
- roles
|
||||||
|
- databases
|
||||||
|
properties:
|
||||||
|
roles:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Role"
|
||||||
|
databases:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/Database"
|
||||||
|
|
||||||
|
Database:
|
||||||
|
type: object
|
||||||
|
description: Database
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- owner
|
||||||
|
- restrict_conn
|
||||||
|
- invalid
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
owner:
|
||||||
|
type: string
|
||||||
|
options:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/GenericOption"
|
||||||
|
restrict_conn:
|
||||||
|
type: boolean
|
||||||
|
invalid:
|
||||||
|
type: boolean
|
||||||
|
|
||||||
|
Role:
|
||||||
|
type: object
|
||||||
|
description: Role
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
encrypted_password:
|
||||||
|
type: string
|
||||||
|
options:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: "#/components/schemas/GenericOption"
|
||||||
|
|
||||||
|
GenericOption:
|
||||||
|
type: object
|
||||||
|
description: Schema Generic option
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- vartype
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
vartype:
|
||||||
|
type: string
|
||||||
|
|
||||||
ComputeState:
|
ComputeState:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
|
|||||||
@@ -8,10 +8,12 @@ pub mod configurator;
|
|||||||
pub mod http;
|
pub mod http;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
pub mod catalog;
|
||||||
pub mod compute;
|
pub mod compute;
|
||||||
pub mod extension_server;
|
pub mod extension_server;
|
||||||
pub mod monitor;
|
pub mod monitor;
|
||||||
pub mod params;
|
pub mod params;
|
||||||
pub mod pg_helpers;
|
pub mod pg_helpers;
|
||||||
pub mod spec;
|
pub mod spec;
|
||||||
|
pub mod swap;
|
||||||
pub mod sync_sk;
|
pub mod sync_sk;
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER ROLE neon_superuser BYPASSRLS;
|
||||||
18
compute_tools/src/migrations/0001-alter_roles.sql
Normal file
18
compute_tools/src/migrations/0001-alter_roles.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
role_name text;
|
||||||
|
BEGIN
|
||||||
|
FOR role_name IN SELECT rolname FROM pg_roles WHERE pg_has_role(rolname, 'neon_superuser', 'member')
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE 'EXECUTING ALTER ROLE % INHERIT', quote_ident(role_name);
|
||||||
|
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' INHERIT';
|
||||||
|
END LOOP;
|
||||||
|
|
||||||
|
FOR role_name IN SELECT rolname FROM pg_roles
|
||||||
|
WHERE
|
||||||
|
NOT pg_has_role(rolname, 'neon_superuser', 'member') AND NOT starts_with(rolname, 'pg_')
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE 'EXECUTING ALTER ROLE % NOBYPASSRLS', quote_ident(role_name);
|
||||||
|
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOBYPASSRLS';
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF (SELECT setting::numeric >= 160000 FROM pg_settings WHERE name = 'server_version_num') THEN
|
||||||
|
EXECUTE 'GRANT pg_create_subscription TO neon_superuser';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
GRANT pg_monitor TO neon_superuser WITH ADMIN OPTION;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- SKIP: Deemed insufficient for allowing relations created by extensions to be
|
||||||
|
-- interacted with by neon_superuser without permission issues.
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO neon_superuser;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- SKIP: Deemed insufficient for allowing relations created by extensions to be
|
||||||
|
-- interacted with by neon_superuser without permission issues.
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO neon_superuser;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- SKIP: Moved inline to the handle_grants() functions.
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO neon_superuser WITH GRANT OPTION;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- SKIP: Moved inline to the handle_grants() functions.
|
||||||
|
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO neon_superuser WITH GRANT OPTION;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
-- SKIP: The original goal of this migration was to prevent creating
|
||||||
|
-- subscriptions, but this migration was insufficient.
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE
|
||||||
|
role_name TEXT;
|
||||||
|
BEGIN
|
||||||
|
FOR role_name IN SELECT rolname FROM pg_roles WHERE rolreplication IS TRUE
|
||||||
|
LOOP
|
||||||
|
RAISE NOTICE 'EXECUTING ALTER ROLE % NOREPLICATION', quote_ident(role_name);
|
||||||
|
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOREPLICATION';
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
@@ -17,7 +17,11 @@ const MONITOR_CHECK_INTERVAL: Duration = Duration::from_millis(500);
|
|||||||
// should be handled gracefully.
|
// should be handled gracefully.
|
||||||
fn watch_compute_activity(compute: &ComputeNode) {
|
fn watch_compute_activity(compute: &ComputeNode) {
|
||||||
// Suppose that `connstr` doesn't change
|
// Suppose that `connstr` doesn't change
|
||||||
let connstr = compute.connstr.as_str();
|
let mut connstr = compute.connstr.clone();
|
||||||
|
connstr
|
||||||
|
.query_pairs_mut()
|
||||||
|
.append_pair("application_name", "compute_activity_monitor");
|
||||||
|
let connstr = connstr.as_str();
|
||||||
|
|
||||||
// During startup and configuration we connect to every Postgres database,
|
// During startup and configuration we connect to every Postgres database,
|
||||||
// but we don't want to count this as some user activity. So wait until
|
// but we don't want to count this as some user activity. So wait until
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ pub fn escape_conf_value(s: &str) -> String {
|
|||||||
format!("'{}'", res)
|
format!("'{}'", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
trait GenericOptionExt {
|
pub trait GenericOptionExt {
|
||||||
fn to_pg_option(&self) -> String;
|
fn to_pg_option(&self) -> String;
|
||||||
fn to_pg_setting(&self) -> String;
|
fn to_pg_setting(&self) -> String;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use std::fs::File;
|
|||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
use postgres::config::Config;
|
use postgres::config::Config;
|
||||||
use postgres::{Client, NoTls};
|
use postgres::{Client, NoTls};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
@@ -302,9 +302,9 @@ pub fn handle_roles(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
RoleAction::Create => {
|
RoleAction::Create => {
|
||||||
// This branch only runs when roles are created through the console, so it is
|
// This branch only runs when roles are created through the console, so it is
|
||||||
// safe to add more permissions here. BYPASSRLS and REPLICATION are inherited
|
// safe to add more permissions here. BYPASSRLS and REPLICATION are inherited
|
||||||
// from neon_superuser. (NOTE: REPLICATION has been removed from here for now).
|
// from neon_superuser.
|
||||||
let mut query: String = format!(
|
let mut query: String = format!(
|
||||||
"CREATE ROLE {} INHERIT CREATEROLE CREATEDB BYPASSRLS IN ROLE neon_superuser",
|
"CREATE ROLE {} INHERIT CREATEROLE CREATEDB BYPASSRLS REPLICATION IN ROLE neon_superuser",
|
||||||
name.pg_quote()
|
name.pg_quote()
|
||||||
);
|
);
|
||||||
info!("running role create query: '{}'", &query);
|
info!("running role create query: '{}'", &query);
|
||||||
@@ -490,7 +490,7 @@ pub fn handle_databases(spec: &ComputeSpec, client: &mut Client) -> Result<()> {
|
|||||||
"rename_db" => {
|
"rename_db" => {
|
||||||
let new_name = op.new_name.as_ref().unwrap();
|
let new_name = op.new_name.as_ref().unwrap();
|
||||||
|
|
||||||
if existing_dbs.get(&op.name).is_some() {
|
if existing_dbs.contains_key(&op.name) {
|
||||||
let query: String = format!(
|
let query: String = format!(
|
||||||
"ALTER DATABASE {} RENAME TO {}",
|
"ALTER DATABASE {} RENAME TO {}",
|
||||||
op.name.pg_quote(),
|
op.name.pg_quote(),
|
||||||
@@ -698,7 +698,8 @@ pub fn handle_grants(
|
|||||||
|
|
||||||
// it is important to run this after all grants
|
// it is important to run this after all grants
|
||||||
if enable_anon_extension {
|
if enable_anon_extension {
|
||||||
handle_extension_anon(spec, &db.owner, &mut db_client, false)?;
|
handle_extension_anon(spec, &db.owner, &mut db_client, false)
|
||||||
|
.context("handle_grants handle_extension_anon")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -743,21 +744,24 @@ pub fn handle_extension_neon(client: &mut Client) -> Result<()> {
|
|||||||
// which may happen in two cases:
|
// which may happen in two cases:
|
||||||
// - extension was just installed
|
// - extension was just installed
|
||||||
// - extension was already installed and is up to date
|
// - extension was already installed and is up to date
|
||||||
// DISABLED due to compute node unpinning epic
|
let query = "ALTER EXTENSION neon UPDATE";
|
||||||
// let query = "ALTER EXTENSION neon UPDATE";
|
info!("update neon extension version with query: {}", query);
|
||||||
// info!("update neon extension version with query: {}", query);
|
if let Err(e) = client.simple_query(query) {
|
||||||
// client.simple_query(query)?;
|
error!(
|
||||||
|
"failed to upgrade neon extension during `handle_extension_neon`: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[instrument(skip_all)]
|
#[instrument(skip_all)]
|
||||||
pub fn handle_neon_extension_upgrade(_client: &mut Client) -> Result<()> {
|
pub fn handle_neon_extension_upgrade(client: &mut Client) -> Result<()> {
|
||||||
info!("handle neon extension upgrade (not really)");
|
info!("handle neon extension upgrade");
|
||||||
// DISABLED due to compute node unpinning epic
|
let query = "ALTER EXTENSION neon UPDATE";
|
||||||
// let query = "ALTER EXTENSION neon UPDATE";
|
info!("update neon extension version with query: {}", query);
|
||||||
// info!("update neon extension version with query: {}", query);
|
client.simple_query(query)?;
|
||||||
// client.simple_query(query)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -770,87 +774,66 @@ pub fn handle_migrations(client: &mut Client) -> Result<()> {
|
|||||||
// !BE SURE TO ONLY ADD MIGRATIONS TO THE END OF THIS ARRAY. IF YOU DO NOT, VERY VERY BAD THINGS MAY HAPPEN!
|
// !BE SURE TO ONLY ADD MIGRATIONS TO THE END OF THIS ARRAY. IF YOU DO NOT, VERY VERY BAD THINGS MAY HAPPEN!
|
||||||
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
// Add new migrations in numerical order.
|
||||||
let migrations = [
|
let migrations = [
|
||||||
"ALTER ROLE neon_superuser BYPASSRLS",
|
include_str!("./migrations/0000-neon_superuser_bypass_rls.sql"),
|
||||||
r#"
|
include_str!("./migrations/0001-alter_roles.sql"),
|
||||||
DO $$
|
include_str!("./migrations/0002-grant_pg_create_subscription_to_neon_superuser.sql"),
|
||||||
DECLARE
|
include_str!("./migrations/0003-grant_pg_monitor_to_neon_superuser.sql"),
|
||||||
role_name text;
|
include_str!("./migrations/0004-grant_all_on_tables_to_neon_superuser.sql"),
|
||||||
BEGIN
|
include_str!("./migrations/0005-grant_all_on_sequences_to_neon_superuser.sql"),
|
||||||
FOR role_name IN SELECT rolname FROM pg_roles WHERE pg_has_role(rolname, 'neon_superuser', 'member')
|
include_str!(
|
||||||
LOOP
|
"./migrations/0006-grant_all_on_tables_to_neon_superuser_with_grant_option.sql"
|
||||||
RAISE NOTICE 'EXECUTING ALTER ROLE % INHERIT', quote_ident(role_name);
|
),
|
||||||
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' INHERIT';
|
include_str!(
|
||||||
END LOOP;
|
"./migrations/0007-grant_all_on_sequences_to_neon_superuser_with_grant_option.sql"
|
||||||
|
),
|
||||||
FOR role_name IN SELECT rolname FROM pg_roles
|
include_str!("./migrations/0008-revoke_replication_for_previously_allowed_roles.sql"),
|
||||||
WHERE
|
|
||||||
NOT pg_has_role(rolname, 'neon_superuser', 'member') AND NOT starts_with(rolname, 'pg_')
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE 'EXECUTING ALTER ROLE % NOBYPASSRLS', quote_ident(role_name);
|
|
||||||
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOBYPASSRLS';
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
"#,
|
|
||||||
r#"
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF (SELECT setting::numeric >= 160000 FROM pg_settings WHERE name = 'server_version_num') THEN
|
|
||||||
EXECUTE 'GRANT pg_create_subscription TO neon_superuser';
|
|
||||||
END IF;
|
|
||||||
END
|
|
||||||
$$;"#,
|
|
||||||
"GRANT pg_monitor TO neon_superuser WITH ADMIN OPTION",
|
|
||||||
// Don't remove: these are some SQLs that we originally applied in migrations but turned out to execute somewhere else.
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
// Add new migrations below.
|
|
||||||
r#"
|
|
||||||
DO $$
|
|
||||||
DECLARE
|
|
||||||
role_name TEXT;
|
|
||||||
BEGIN
|
|
||||||
FOR role_name IN SELECT rolname FROM pg_roles WHERE rolreplication IS TRUE
|
|
||||||
LOOP
|
|
||||||
RAISE NOTICE 'EXECUTING ALTER ROLE % NOREPLICATION', quote_ident(role_name);
|
|
||||||
EXECUTE 'ALTER ROLE ' || quote_ident(role_name) || ' NOREPLICATION';
|
|
||||||
END LOOP;
|
|
||||||
END
|
|
||||||
$$;"#,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut query = "CREATE SCHEMA IF NOT EXISTS neon_migration";
|
let mut func = || {
|
||||||
client.simple_query(query)?;
|
let query = "CREATE SCHEMA IF NOT EXISTS neon_migration";
|
||||||
|
client.simple_query(query)?;
|
||||||
|
|
||||||
query = "CREATE TABLE IF NOT EXISTS neon_migration.migration_id (key INT NOT NULL PRIMARY KEY, id bigint NOT NULL DEFAULT 0)";
|
let query = "CREATE TABLE IF NOT EXISTS neon_migration.migration_id (key INT NOT NULL PRIMARY KEY, id bigint NOT NULL DEFAULT 0)";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
|
|
||||||
query = "INSERT INTO neon_migration.migration_id VALUES (0, 0) ON CONFLICT DO NOTHING";
|
let query = "INSERT INTO neon_migration.migration_id VALUES (0, 0) ON CONFLICT DO NOTHING";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
|
|
||||||
query = "ALTER SCHEMA neon_migration OWNER TO cloud_admin";
|
let query = "ALTER SCHEMA neon_migration OWNER TO cloud_admin";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
|
|
||||||
query = "REVOKE ALL ON SCHEMA neon_migration FROM PUBLIC";
|
let query = "REVOKE ALL ON SCHEMA neon_migration FROM PUBLIC";
|
||||||
client.simple_query(query)?;
|
client.simple_query(query)?;
|
||||||
|
Ok::<_, anyhow::Error>(())
|
||||||
|
};
|
||||||
|
func().context("handle_migrations prepare")?;
|
||||||
|
|
||||||
query = "SELECT id FROM neon_migration.migration_id";
|
let query = "SELECT id FROM neon_migration.migration_id";
|
||||||
let row = client.query_one(query, &[])?;
|
let row = client
|
||||||
|
.query_one(query, &[])
|
||||||
|
.context("handle_migrations get migration_id")?;
|
||||||
let mut current_migration: usize = row.get::<&str, i64>("id") as usize;
|
let mut current_migration: usize = row.get::<&str, i64>("id") as usize;
|
||||||
let starting_migration_id = current_migration;
|
let starting_migration_id = current_migration;
|
||||||
|
|
||||||
query = "BEGIN";
|
let query = "BEGIN";
|
||||||
client.simple_query(query)?;
|
client
|
||||||
|
.simple_query(query)
|
||||||
|
.context("handle_migrations begin")?;
|
||||||
|
|
||||||
while current_migration < migrations.len() {
|
while current_migration < migrations.len() {
|
||||||
let migration = &migrations[current_migration];
|
let migration = &migrations[current_migration];
|
||||||
if migration.is_empty() {
|
if migration.starts_with("-- SKIP") {
|
||||||
info!("Skip migration id={}", current_migration);
|
info!("Skipping migration id={}", current_migration);
|
||||||
} else {
|
} else {
|
||||||
info!("Running migration:\n{}\n", migration);
|
info!(
|
||||||
client.simple_query(migration)?;
|
"Running migration id={}:\n{}\n",
|
||||||
|
current_migration, migration
|
||||||
|
);
|
||||||
|
client.simple_query(migration).with_context(|| {
|
||||||
|
format!("handle_migrations current_migration={}", current_migration)
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
current_migration += 1;
|
current_migration += 1;
|
||||||
}
|
}
|
||||||
@@ -858,10 +841,14 @@ $$;"#,
|
|||||||
"UPDATE neon_migration.migration_id SET id={}",
|
"UPDATE neon_migration.migration_id SET id={}",
|
||||||
migrations.len()
|
migrations.len()
|
||||||
);
|
);
|
||||||
client.simple_query(&setval)?;
|
client
|
||||||
|
.simple_query(&setval)
|
||||||
|
.context("handle_migrations update id")?;
|
||||||
|
|
||||||
query = "COMMIT";
|
let query = "COMMIT";
|
||||||
client.simple_query(query)?;
|
client
|
||||||
|
.simple_query(query)
|
||||||
|
.context("handle_migrations commit")?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Ran {} migrations",
|
"Ran {} migrations",
|
||||||
|
|||||||
45
compute_tools/src/swap.rs
Normal file
45
compute_tools/src/swap.rs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Context};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
pub const RESIZE_SWAP_BIN: &str = "/neonvm/bin/resize-swap";
|
||||||
|
|
||||||
|
pub fn resize_swap(size_bytes: u64) -> anyhow::Result<()> {
|
||||||
|
// run `/neonvm/bin/resize-swap --once {size_bytes}`
|
||||||
|
//
|
||||||
|
// Passing '--once' causes resize-swap to delete itself after successful completion, which
|
||||||
|
// means that if compute_ctl restarts later, we won't end up calling 'swapoff' while
|
||||||
|
// postgres is running.
|
||||||
|
//
|
||||||
|
// NOTE: resize-swap is not very clever. If present, --once MUST be the first arg.
|
||||||
|
let child_result = std::process::Command::new("/usr/bin/sudo")
|
||||||
|
.arg(RESIZE_SWAP_BIN)
|
||||||
|
.arg("--once")
|
||||||
|
.arg(size_bytes.to_string())
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
child_result
|
||||||
|
.context("spawn() failed")
|
||||||
|
.and_then(|mut child| child.wait().context("wait() failed"))
|
||||||
|
.and_then(|status| match status.success() {
|
||||||
|
true => Ok(()),
|
||||||
|
false => {
|
||||||
|
// The command failed. Maybe it was because the resize-swap file doesn't exist?
|
||||||
|
// The --once flag causes it to delete itself on success so we don't disable swap
|
||||||
|
// while postgres is running; maybe this is fine.
|
||||||
|
match Path::new(RESIZE_SWAP_BIN).try_exists() {
|
||||||
|
Err(_) | Ok(true) => Err(anyhow!("process exited with {status}")),
|
||||||
|
// The path doesn't exist; we're actually ok
|
||||||
|
Ok(false) => {
|
||||||
|
warn!("ignoring \"not found\" error from resize-swap to avoid swapoff while compute is running");
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// wrap any prior error with the overall context that we couldn't run the command
|
||||||
|
.with_context(|| {
|
||||||
|
format!("could not run `/usr/bin/sudo {RESIZE_SWAP_BIN} --once {size_bytes}`")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ nix.workspace = true
|
|||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
postgres.workspace = true
|
postgres.workspace = true
|
||||||
hex.workspace = true
|
hex.workspace = true
|
||||||
|
humantime-serde.workspace = true
|
||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
regex.workspace = true
|
regex.workspace = true
|
||||||
reqwest = { workspace = true, features = ["blocking", "json"] }
|
reqwest = { workspace = true, features = ["blocking", "json"] }
|
||||||
@@ -27,6 +28,7 @@ serde_with.workspace = true
|
|||||||
tar.workspace = true
|
tar.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
toml.workspace = true
|
toml.workspace = true
|
||||||
|
toml_edit.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
tokio-postgres.workspace = true
|
tokio-postgres.workspace = true
|
||||||
tokio-util.workspace = true
|
tokio-util.workspace = true
|
||||||
|
|||||||
@@ -1,462 +0,0 @@
|
|||||||
use std::{collections::HashMap, time::Duration};
|
|
||||||
|
|
||||||
use control_plane::endpoint::{ComputeControlPlane, EndpointStatus};
|
|
||||||
use control_plane::local_env::LocalEnv;
|
|
||||||
use hyper::{Method, StatusCode};
|
|
||||||
use pageserver_api::shard::{ShardCount, ShardNumber, ShardStripeSize, TenantShardId};
|
|
||||||
use postgres_connection::parse_host_port;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tokio_util::sync::CancellationToken;
|
|
||||||
use utils::{
|
|
||||||
backoff::{self},
|
|
||||||
id::{NodeId, TenantId},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::service::Config;
|
|
||||||
|
|
||||||
const BUSY_DELAY: Duration = Duration::from_secs(1);
|
|
||||||
const SLOWDOWN_DELAY: Duration = Duration::from_secs(5);
|
|
||||||
|
|
||||||
pub(crate) const API_CONCURRENCY: usize = 32;
|
|
||||||
|
|
||||||
struct ShardedComputeHookTenant {
|
|
||||||
stripe_size: ShardStripeSize,
|
|
||||||
shard_count: ShardCount,
|
|
||||||
shards: Vec<(ShardNumber, NodeId)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ComputeHookTenant {
|
|
||||||
Unsharded(NodeId),
|
|
||||||
Sharded(ShardedComputeHookTenant),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ComputeHookTenant {
|
|
||||||
/// Construct with at least one shard's information
|
|
||||||
fn new(tenant_shard_id: TenantShardId, stripe_size: ShardStripeSize, node_id: NodeId) -> Self {
|
|
||||||
if tenant_shard_id.shard_count.count() > 1 {
|
|
||||||
Self::Sharded(ShardedComputeHookTenant {
|
|
||||||
shards: vec![(tenant_shard_id.shard_number, node_id)],
|
|
||||||
stripe_size,
|
|
||||||
shard_count: tenant_shard_id.shard_count,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
Self::Unsharded(node_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set one shard's location. If stripe size or shard count have changed, Self is reset
|
|
||||||
/// and drops existing content.
|
|
||||||
fn update(
|
|
||||||
&mut self,
|
|
||||||
tenant_shard_id: TenantShardId,
|
|
||||||
stripe_size: ShardStripeSize,
|
|
||||||
node_id: NodeId,
|
|
||||||
) {
|
|
||||||
match self {
|
|
||||||
Self::Unsharded(existing_node_id) if tenant_shard_id.shard_count.count() == 1 => {
|
|
||||||
*existing_node_id = node_id
|
|
||||||
}
|
|
||||||
Self::Sharded(sharded_tenant)
|
|
||||||
if sharded_tenant.stripe_size == stripe_size
|
|
||||||
&& sharded_tenant.shard_count == tenant_shard_id.shard_count =>
|
|
||||||
{
|
|
||||||
if let Some(existing) = sharded_tenant
|
|
||||||
.shards
|
|
||||||
.iter()
|
|
||||||
.position(|s| s.0 == tenant_shard_id.shard_number)
|
|
||||||
{
|
|
||||||
sharded_tenant.shards.get_mut(existing).unwrap().1 = node_id;
|
|
||||||
} else {
|
|
||||||
sharded_tenant
|
|
||||||
.shards
|
|
||||||
.push((tenant_shard_id.shard_number, node_id));
|
|
||||||
sharded_tenant.shards.sort_by_key(|s| s.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Shard count changed: reset struct.
|
|
||||||
*self = Self::new(tenant_shard_id, stripe_size, node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
struct ComputeHookNotifyRequestShard {
|
|
||||||
node_id: NodeId,
|
|
||||||
shard_number: ShardNumber,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Request body that we send to the control plane to notify it of where a tenant is attached
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
struct ComputeHookNotifyRequest {
|
|
||||||
tenant_id: TenantId,
|
|
||||||
stripe_size: Option<ShardStripeSize>,
|
|
||||||
shards: Vec<ComputeHookNotifyRequestShard>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Error type for attempts to call into the control plane compute notification hook
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub(crate) enum NotifyError {
|
|
||||||
// Request was not send successfully, e.g. transport error
|
|
||||||
#[error("Sending request: {0}")]
|
|
||||||
Request(#[from] reqwest::Error),
|
|
||||||
// Request could not be serviced right now due to ongoing Operation in control plane, but should be possible soon.
|
|
||||||
#[error("Control plane tenant busy")]
|
|
||||||
Busy,
|
|
||||||
// Explicit 429 response asking us to retry less frequently
|
|
||||||
#[error("Control plane overloaded")]
|
|
||||||
SlowDown,
|
|
||||||
// A 503 response indicates the control plane can't handle the request right now
|
|
||||||
#[error("Control plane unavailable (status {0})")]
|
|
||||||
Unavailable(StatusCode),
|
|
||||||
// API returned unexpected non-success status. We will retry, but log a warning.
|
|
||||||
#[error("Control plane returned unexpected status {0}")]
|
|
||||||
Unexpected(StatusCode),
|
|
||||||
// We shutdown while sending
|
|
||||||
#[error("Shutting down")]
|
|
||||||
ShuttingDown,
|
|
||||||
// A response indicates we will never succeed, such as 400 or 404
|
|
||||||
#[error("Non-retryable error {0}")]
|
|
||||||
Fatal(StatusCode),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ComputeHookTenant {
|
|
||||||
fn maybe_reconfigure(&self, tenant_id: TenantId) -> Option<ComputeHookNotifyRequest> {
|
|
||||||
match self {
|
|
||||||
Self::Unsharded(node_id) => Some(ComputeHookNotifyRequest {
|
|
||||||
tenant_id,
|
|
||||||
shards: vec![ComputeHookNotifyRequestShard {
|
|
||||||
shard_number: ShardNumber(0),
|
|
||||||
node_id: *node_id,
|
|
||||||
}],
|
|
||||||
stripe_size: None,
|
|
||||||
}),
|
|
||||||
Self::Sharded(sharded_tenant)
|
|
||||||
if sharded_tenant.shards.len() == sharded_tenant.shard_count.count() as usize =>
|
|
||||||
{
|
|
||||||
Some(ComputeHookNotifyRequest {
|
|
||||||
tenant_id,
|
|
||||||
shards: sharded_tenant
|
|
||||||
.shards
|
|
||||||
.iter()
|
|
||||||
.map(|(shard_number, node_id)| ComputeHookNotifyRequestShard {
|
|
||||||
shard_number: *shard_number,
|
|
||||||
node_id: *node_id,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
stripe_size: Some(sharded_tenant.stripe_size),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Self::Sharded(sharded_tenant) => {
|
|
||||||
// Sharded tenant doesn't yet have information for all its shards
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"ComputeHookTenant::maybe_reconfigure: not enough shards ({}/{})",
|
|
||||||
sharded_tenant.shards.len(),
|
|
||||||
sharded_tenant.shard_count.count()
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The compute hook is a destination for notifications about changes to tenant:pageserver
|
|
||||||
/// mapping. It aggregates updates for the shards in a tenant, and when appropriate reconfigures
|
|
||||||
/// the compute connection string.
|
|
||||||
pub(super) struct ComputeHook {
|
|
||||||
config: Config,
|
|
||||||
state: tokio::sync::Mutex<HashMap<TenantId, ComputeHookTenant>>,
|
|
||||||
authorization_header: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ComputeHook {
|
|
||||||
pub(super) fn new(config: Config) -> Self {
|
|
||||||
let authorization_header = config
|
|
||||||
.control_plane_jwt_token
|
|
||||||
.clone()
|
|
||||||
.map(|jwt| format!("Bearer {}", jwt));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
state: Default::default(),
|
|
||||||
config,
|
|
||||||
authorization_header,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For test environments: use neon_local's LocalEnv to update compute
|
|
||||||
async fn do_notify_local(
|
|
||||||
&self,
|
|
||||||
reconfigure_request: ComputeHookNotifyRequest,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let env = match LocalEnv::load_config() {
|
|
||||||
Ok(e) => e,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::warn!("Couldn't load neon_local config, skipping compute update ({e})");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let cplane =
|
|
||||||
ComputeControlPlane::load(env.clone()).expect("Error loading compute control plane");
|
|
||||||
let ComputeHookNotifyRequest {
|
|
||||||
tenant_id,
|
|
||||||
shards,
|
|
||||||
stripe_size,
|
|
||||||
} = reconfigure_request;
|
|
||||||
|
|
||||||
let compute_pageservers = shards
|
|
||||||
.into_iter()
|
|
||||||
.map(|shard| {
|
|
||||||
let ps_conf = env
|
|
||||||
.get_pageserver_conf(shard.node_id)
|
|
||||||
.expect("Unknown pageserver");
|
|
||||||
let (pg_host, pg_port) = parse_host_port(&ps_conf.listen_pg_addr)
|
|
||||||
.expect("Unable to parse listen_pg_addr");
|
|
||||||
(pg_host, pg_port.unwrap_or(5432))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for (endpoint_name, endpoint) in &cplane.endpoints {
|
|
||||||
if endpoint.tenant_id == tenant_id && endpoint.status() == EndpointStatus::Running {
|
|
||||||
tracing::info!("Reconfiguring endpoint {}", endpoint_name,);
|
|
||||||
endpoint
|
|
||||||
.reconfigure(compute_pageservers.clone(), stripe_size)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn do_notify_iteration(
|
|
||||||
&self,
|
|
||||||
client: &reqwest::Client,
|
|
||||||
url: &String,
|
|
||||||
reconfigure_request: &ComputeHookNotifyRequest,
|
|
||||||
cancel: &CancellationToken,
|
|
||||||
) -> Result<(), NotifyError> {
|
|
||||||
let req = client.request(Method::PUT, url);
|
|
||||||
let req = if let Some(value) = &self.authorization_header {
|
|
||||||
req.header(reqwest::header::AUTHORIZATION, value)
|
|
||||||
} else {
|
|
||||||
req
|
|
||||||
};
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"Sending notify request to {} ({:?})",
|
|
||||||
url,
|
|
||||||
reconfigure_request
|
|
||||||
);
|
|
||||||
let send_result = req.json(&reconfigure_request).send().await;
|
|
||||||
let response = match send_result {
|
|
||||||
Ok(r) => r,
|
|
||||||
Err(e) => return Err(e.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Treat all 2xx responses as success
|
|
||||||
if response.status() >= StatusCode::OK && response.status() < StatusCode::MULTIPLE_CHOICES {
|
|
||||||
if response.status() != StatusCode::OK {
|
|
||||||
// Non-200 2xx response: it doesn't make sense to retry, but this is unexpected, so
|
|
||||||
// log a warning.
|
|
||||||
tracing::warn!(
|
|
||||||
"Unexpected 2xx response code {} from control plane",
|
|
||||||
response.status()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error response codes
|
|
||||||
match response.status() {
|
|
||||||
StatusCode::TOO_MANY_REQUESTS => {
|
|
||||||
// TODO: 429 handling should be global: set some state visible to other requests
|
|
||||||
// so that they will delay before starting, rather than all notifications trying
|
|
||||||
// once before backing off.
|
|
||||||
tokio::time::timeout(SLOWDOWN_DELAY, cancel.cancelled())
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
Err(NotifyError::SlowDown)
|
|
||||||
}
|
|
||||||
StatusCode::LOCKED => {
|
|
||||||
// Delay our retry if busy: the usual fast exponential backoff in backoff::retry
|
|
||||||
// is not appropriate
|
|
||||||
tokio::time::timeout(BUSY_DELAY, cancel.cancelled())
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
Err(NotifyError::Busy)
|
|
||||||
}
|
|
||||||
StatusCode::SERVICE_UNAVAILABLE
|
|
||||||
| StatusCode::GATEWAY_TIMEOUT
|
|
||||||
| StatusCode::BAD_GATEWAY => Err(NotifyError::Unavailable(response.status())),
|
|
||||||
StatusCode::BAD_REQUEST | StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => {
|
|
||||||
Err(NotifyError::Fatal(response.status()))
|
|
||||||
}
|
|
||||||
_ => Err(NotifyError::Unexpected(response.status())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn do_notify(
|
|
||||||
&self,
|
|
||||||
url: &String,
|
|
||||||
reconfigure_request: ComputeHookNotifyRequest,
|
|
||||||
cancel: &CancellationToken,
|
|
||||||
) -> Result<(), NotifyError> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
backoff::retry(
|
|
||||||
|| self.do_notify_iteration(&client, url, &reconfigure_request, cancel),
|
|
||||||
|e| matches!(e, NotifyError::Fatal(_) | NotifyError::Unexpected(_)),
|
|
||||||
3,
|
|
||||||
10,
|
|
||||||
"Send compute notification",
|
|
||||||
cancel,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok_or_else(|| NotifyError::ShuttingDown)
|
|
||||||
.and_then(|x| x)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Call this to notify the compute (postgres) tier of new pageservers to use
|
|
||||||
/// for a tenant. notify() is called by each shard individually, and this function
|
|
||||||
/// will decide whether an update to the tenant is sent. An update is sent on the
|
|
||||||
/// condition that:
|
|
||||||
/// - We know a pageserver for every shard.
|
|
||||||
/// - All the shards have the same shard_count (i.e. we are not mid-split)
|
|
||||||
///
|
|
||||||
/// Cancellation token enables callers to drop out, e.g. if calling from a Reconciler
|
|
||||||
/// that is cancelled.
|
|
||||||
///
|
|
||||||
/// This function is fallible, including in the case that the control plane is transiently
|
|
||||||
/// unavailable. A limited number of retries are done internally to efficiently hide short unavailability
|
|
||||||
/// periods, but we don't retry forever. The **caller** is responsible for handling failures and
|
|
||||||
/// ensuring that they eventually call again to ensure that the compute is eventually notified of
|
|
||||||
/// the proper pageserver nodes for a tenant.
|
|
||||||
#[tracing::instrument(skip_all, fields(tenant_id=%tenant_shard_id.tenant_id, shard_id=%tenant_shard_id.shard_slug(), node_id))]
|
|
||||||
pub(super) async fn notify(
|
|
||||||
&self,
|
|
||||||
tenant_shard_id: TenantShardId,
|
|
||||||
node_id: NodeId,
|
|
||||||
stripe_size: ShardStripeSize,
|
|
||||||
cancel: &CancellationToken,
|
|
||||||
) -> Result<(), NotifyError> {
|
|
||||||
let mut locked = self.state.lock().await;
|
|
||||||
|
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
let tenant = match locked.entry(tenant_shard_id.tenant_id) {
|
|
||||||
Entry::Vacant(e) => e.insert(ComputeHookTenant::new(
|
|
||||||
tenant_shard_id,
|
|
||||||
stripe_size,
|
|
||||||
node_id,
|
|
||||||
)),
|
|
||||||
Entry::Occupied(e) => {
|
|
||||||
let tenant = e.into_mut();
|
|
||||||
tenant.update(tenant_shard_id, stripe_size, node_id);
|
|
||||||
tenant
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let reconfigure_request = tenant.maybe_reconfigure(tenant_shard_id.tenant_id);
|
|
||||||
let Some(reconfigure_request) = reconfigure_request else {
|
|
||||||
// The tenant doesn't yet have pageservers for all its shards: we won't notify anything
|
|
||||||
// until it does.
|
|
||||||
tracing::info!("Tenant isn't yet ready to emit a notification");
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(notify_url) = &self.config.compute_hook_url {
|
|
||||||
self.do_notify(notify_url, reconfigure_request, cancel)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
self.do_notify_local(reconfigure_request)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
// This path is for testing only, so munge the error into our prod-style error type.
|
|
||||||
tracing::error!("Local notification hook failed: {e}");
|
|
||||||
NotifyError::Fatal(StatusCode::INTERNAL_SERVER_ERROR)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) mod tests {
|
|
||||||
use pageserver_api::shard::{ShardCount, ShardNumber};
|
|
||||||
use utils::id::TenantId;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tenant_updates() -> anyhow::Result<()> {
|
|
||||||
let tenant_id = TenantId::generate();
|
|
||||||
let mut tenant_state = ComputeHookTenant::new(
|
|
||||||
TenantShardId {
|
|
||||||
tenant_id,
|
|
||||||
shard_count: ShardCount::new(0),
|
|
||||||
shard_number: ShardNumber(0),
|
|
||||||
},
|
|
||||||
ShardStripeSize(12345),
|
|
||||||
NodeId(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
// An unsharded tenant is always ready to emit a notification
|
|
||||||
assert!(tenant_state.maybe_reconfigure(tenant_id).is_some());
|
|
||||||
assert_eq!(
|
|
||||||
tenant_state
|
|
||||||
.maybe_reconfigure(tenant_id)
|
|
||||||
.unwrap()
|
|
||||||
.shards
|
|
||||||
.len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
assert!(tenant_state
|
|
||||||
.maybe_reconfigure(tenant_id)
|
|
||||||
.unwrap()
|
|
||||||
.stripe_size
|
|
||||||
.is_none());
|
|
||||||
|
|
||||||
// Writing the first shard of a multi-sharded situation (i.e. in a split)
|
|
||||||
// resets the tenant state and puts it in an non-notifying state (need to
|
|
||||||
// see all shards)
|
|
||||||
tenant_state.update(
|
|
||||||
TenantShardId {
|
|
||||||
tenant_id,
|
|
||||||
shard_count: ShardCount::new(2),
|
|
||||||
shard_number: ShardNumber(1),
|
|
||||||
},
|
|
||||||
ShardStripeSize(32768),
|
|
||||||
NodeId(1),
|
|
||||||
);
|
|
||||||
assert!(tenant_state.maybe_reconfigure(tenant_id).is_none());
|
|
||||||
|
|
||||||
// Writing the second shard makes it ready to notify
|
|
||||||
tenant_state.update(
|
|
||||||
TenantShardId {
|
|
||||||
tenant_id,
|
|
||||||
shard_count: ShardCount::new(2),
|
|
||||||
shard_number: ShardNumber(0),
|
|
||||||
},
|
|
||||||
ShardStripeSize(32768),
|
|
||||||
NodeId(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(tenant_state.maybe_reconfigure(tenant_id).is_some());
|
|
||||||
assert_eq!(
|
|
||||||
tenant_state
|
|
||||||
.maybe_reconfigure(tenant_id)
|
|
||||||
.unwrap()
|
|
||||||
.shards
|
|
||||||
.len(),
|
|
||||||
2
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
tenant_state
|
|
||||||
.maybe_reconfigure(tenant_id)
|
|
||||||
.unwrap()
|
|
||||||
.stripe_size,
|
|
||||||
Some(ShardStripeSize(32768))
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
|
||||||
|
|
||||||
/// A map of locks covering some arbitrary identifiers. Useful if you have a collection of objects but don't
|
|
||||||
/// want to embed a lock in each one, or if your locking granularity is different to your object granularity.
|
|
||||||
/// For example, used in the storage controller where the objects are tenant shards, but sometimes locking
|
|
||||||
/// is needed at a tenant-wide granularity.
|
|
||||||
pub(crate) struct IdLockMap<T>
|
|
||||||
where
|
|
||||||
T: Eq + PartialEq + std::hash::Hash,
|
|
||||||
{
|
|
||||||
/// A synchronous lock for getting/setting the async locks that our callers will wait on.
|
|
||||||
entities: std::sync::Mutex<std::collections::HashMap<T, Arc<tokio::sync::RwLock<()>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> IdLockMap<T>
|
|
||||||
where
|
|
||||||
T: Eq + PartialEq + std::hash::Hash,
|
|
||||||
{
|
|
||||||
pub(crate) fn shared(
|
|
||||||
&self,
|
|
||||||
key: T,
|
|
||||||
) -> impl std::future::Future<Output = tokio::sync::OwnedRwLockReadGuard<()>> {
|
|
||||||
let mut locked = self.entities.lock().unwrap();
|
|
||||||
let entry = locked.entry(key).or_default();
|
|
||||||
entry.clone().read_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn exclusive(
|
|
||||||
&self,
|
|
||||||
key: T,
|
|
||||||
) -> impl std::future::Future<Output = tokio::sync::OwnedRwLockWriteGuard<()>> {
|
|
||||||
let mut locked = self.entities.lock().unwrap();
|
|
||||||
let entry = locked.entry(key).or_default();
|
|
||||||
entry.clone().write_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rather than building a lock guard that re-takes the [`Self::entities`] lock, we just do
|
|
||||||
/// periodic housekeeping to avoid the map growing indefinitely
|
|
||||||
pub(crate) fn housekeeping(&self) {
|
|
||||||
let mut locked = self.entities.lock().unwrap();
|
|
||||||
locked.retain(|_k, lock| lock.try_write().is_err())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for IdLockMap<T>
|
|
||||||
where
|
|
||||||
T: Eq + PartialEq + std::hash::Hash,
|
|
||||||
{
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
entities: std::sync::Mutex::new(HashMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
use crate::{node::Node, tenant_state::TenantState};
|
|
||||||
use pageserver_api::controller_api::UtilizationScore;
|
|
||||||
use serde::Serialize;
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use utils::{http::error::ApiError, id::NodeId};
|
|
||||||
|
|
||||||
/// Scenarios in which we cannot find a suitable location for a tenant shard
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum ScheduleError {
|
|
||||||
#[error("No pageservers found")]
|
|
||||||
NoPageservers,
|
|
||||||
#[error("No pageserver found matching constraint")]
|
|
||||||
ImpossibleConstraint,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ScheduleError> for ApiError {
|
|
||||||
fn from(value: ScheduleError) -> Self {
|
|
||||||
ApiError::Conflict(format!("Scheduling error: {}", value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Eq, PartialEq)]
|
|
||||||
pub enum MaySchedule {
|
|
||||||
Yes(UtilizationScore),
|
|
||||||
No,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
struct SchedulerNode {
|
|
||||||
/// How many shards are currently scheduled on this node, via their [`crate::tenant_state::IntentState`].
|
|
||||||
shard_count: usize,
|
|
||||||
|
|
||||||
/// Whether this node is currently elegible to have new shards scheduled (this is derived
|
|
||||||
/// from a node's availability state and scheduling policy).
|
|
||||||
may_schedule: MaySchedule,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for SchedulerNode {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
let may_schedule_matches = matches!(
|
|
||||||
(&self.may_schedule, &other.may_schedule),
|
|
||||||
(MaySchedule::Yes(_), MaySchedule::Yes(_)) | (MaySchedule::No, MaySchedule::No)
|
|
||||||
);
|
|
||||||
|
|
||||||
may_schedule_matches && self.shard_count == other.shard_count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for SchedulerNode {}
|
|
||||||
|
|
||||||
/// This type is responsible for selecting which node is used when a tenant shard needs to choose a pageserver
|
|
||||||
/// on which to run.
|
|
||||||
///
|
|
||||||
/// The type has no persistent state of its own: this is all populated at startup. The Serialize
|
|
||||||
/// impl is only for debug dumps.
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub(crate) struct Scheduler {
|
|
||||||
nodes: HashMap<NodeId, SchedulerNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scheduler {
|
|
||||||
pub(crate) fn new<'a>(nodes: impl Iterator<Item = &'a Node>) -> Self {
|
|
||||||
let mut scheduler_nodes = HashMap::new();
|
|
||||||
for node in nodes {
|
|
||||||
scheduler_nodes.insert(
|
|
||||||
node.get_id(),
|
|
||||||
SchedulerNode {
|
|
||||||
shard_count: 0,
|
|
||||||
may_schedule: node.may_schedule(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
|
||||||
nodes: scheduler_nodes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For debug/support: check that our internal statistics are in sync with the state of
|
|
||||||
/// the nodes & tenant shards.
|
|
||||||
///
|
|
||||||
/// If anything is inconsistent, log details and return an error.
|
|
||||||
pub(crate) fn consistency_check<'a>(
|
|
||||||
&self,
|
|
||||||
nodes: impl Iterator<Item = &'a Node>,
|
|
||||||
shards: impl Iterator<Item = &'a TenantState>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut expect_nodes: HashMap<NodeId, SchedulerNode> = HashMap::new();
|
|
||||||
for node in nodes {
|
|
||||||
expect_nodes.insert(
|
|
||||||
node.get_id(),
|
|
||||||
SchedulerNode {
|
|
||||||
shard_count: 0,
|
|
||||||
may_schedule: node.may_schedule(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for shard in shards {
|
|
||||||
if let Some(node_id) = shard.intent.get_attached() {
|
|
||||||
match expect_nodes.get_mut(node_id) {
|
|
||||||
Some(node) => node.shard_count += 1,
|
|
||||||
None => anyhow::bail!(
|
|
||||||
"Tenant {} references nonexistent node {}",
|
|
||||||
shard.tenant_shard_id,
|
|
||||||
node_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for node_id in shard.intent.get_secondary() {
|
|
||||||
match expect_nodes.get_mut(node_id) {
|
|
||||||
Some(node) => node.shard_count += 1,
|
|
||||||
None => anyhow::bail!(
|
|
||||||
"Tenant {} references nonexistent node {}",
|
|
||||||
shard.tenant_shard_id,
|
|
||||||
node_id
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (node_id, expect_node) in &expect_nodes {
|
|
||||||
let Some(self_node) = self.nodes.get(node_id) else {
|
|
||||||
anyhow::bail!("Node {node_id} not found in Self")
|
|
||||||
};
|
|
||||||
|
|
||||||
if self_node != expect_node {
|
|
||||||
tracing::error!("Inconsistency detected in scheduling state for node {node_id}");
|
|
||||||
tracing::error!("Expected state: {}", serde_json::to_string(expect_node)?);
|
|
||||||
tracing::error!("Self state: {}", serde_json::to_string(self_node)?);
|
|
||||||
|
|
||||||
anyhow::bail!("Inconsistent state on {node_id}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if expect_nodes.len() != self.nodes.len() {
|
|
||||||
// We just checked that all the expected nodes are present. If the lengths don't match,
|
|
||||||
// it means that we have nodes in Self that are unexpected.
|
|
||||||
for node_id in self.nodes.keys() {
|
|
||||||
if !expect_nodes.contains_key(node_id) {
|
|
||||||
anyhow::bail!("Node {node_id} found in Self but not in expected nodes");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Increment the reference count of a node. This reference count is used to guide scheduling
|
|
||||||
/// decisions, not for memory management: it represents one tenant shard whose IntentState targets
|
|
||||||
/// this node.
|
|
||||||
///
|
|
||||||
/// It is an error to call this for a node that is not known to the scheduler (i.e. passed into
|
|
||||||
/// [`Self::new`] or [`Self::node_upsert`])
|
|
||||||
pub(crate) fn node_inc_ref(&mut self, node_id: NodeId) {
|
|
||||||
let Some(node) = self.nodes.get_mut(&node_id) else {
|
|
||||||
tracing::error!("Scheduler missing node {node_id}");
|
|
||||||
debug_assert!(false);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
node.shard_count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decrement a node's reference count. Inverse of [`Self::node_inc_ref`].
|
|
||||||
pub(crate) fn node_dec_ref(&mut self, node_id: NodeId) {
|
|
||||||
let Some(node) = self.nodes.get_mut(&node_id) else {
|
|
||||||
debug_assert!(false);
|
|
||||||
tracing::error!("Scheduler missing node {node_id}");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
node.shard_count -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn node_upsert(&mut self, node: &Node) {
|
|
||||||
use std::collections::hash_map::Entry::*;
|
|
||||||
match self.nodes.entry(node.get_id()) {
|
|
||||||
Occupied(mut entry) => {
|
|
||||||
entry.get_mut().may_schedule = node.may_schedule();
|
|
||||||
}
|
|
||||||
Vacant(entry) => {
|
|
||||||
entry.insert(SchedulerNode {
|
|
||||||
shard_count: 0,
|
|
||||||
may_schedule: node.may_schedule(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn node_remove(&mut self, node_id: NodeId) {
|
|
||||||
if self.nodes.remove(&node_id).is_none() {
|
|
||||||
tracing::warn!(node_id=%node_id, "Removed non-existent node from scheduler");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where we have several nodes to choose from, for example when picking a secondary location
|
|
||||||
/// to promote to an attached location, this method may be used to pick the best choice based
|
|
||||||
/// on the scheduler's knowledge of utilization and availability.
|
|
||||||
///
|
|
||||||
/// If the input is empty, or all the nodes are not elegible for scheduling, return None: the
|
|
||||||
/// caller can pick a node some other way.
|
|
||||||
pub(crate) fn node_preferred(&self, nodes: &[NodeId]) -> Option<NodeId> {
|
|
||||||
if nodes.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: When the utilization score returned by the pageserver becomes meaningful,
|
|
||||||
// schedule based on that instead of the shard count.
|
|
||||||
let node = nodes
|
|
||||||
.iter()
|
|
||||||
.map(|node_id| {
|
|
||||||
let may_schedule = self
|
|
||||||
.nodes
|
|
||||||
.get(node_id)
|
|
||||||
.map(|n| n.may_schedule != MaySchedule::No)
|
|
||||||
.unwrap_or(false);
|
|
||||||
(*node_id, may_schedule)
|
|
||||||
})
|
|
||||||
.max_by_key(|(_n, may_schedule)| *may_schedule);
|
|
||||||
|
|
||||||
// If even the preferred node has may_schedule==false, return None
|
|
||||||
node.and_then(|(node_id, may_schedule)| if may_schedule { Some(node_id) } else { None })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn schedule_shard(&self, hard_exclude: &[NodeId]) -> Result<NodeId, ScheduleError> {
|
|
||||||
if self.nodes.is_empty() {
|
|
||||||
return Err(ScheduleError::NoPageservers);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut tenant_counts: Vec<(NodeId, usize)> = self
|
|
||||||
.nodes
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(k, v)| {
|
|
||||||
if hard_exclude.contains(k) || v.may_schedule == MaySchedule::No {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some((*k, v.shard_count))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
// Sort by tenant count. Nodes with the same tenant count are sorted by ID.
|
|
||||||
tenant_counts.sort_by_key(|i| (i.1, i.0));
|
|
||||||
|
|
||||||
if tenant_counts.is_empty() {
|
|
||||||
// After applying constraints, no pageservers were left. We log some detail about
|
|
||||||
// the state of nodes to help understand why this happened. This is not logged as an error because
|
|
||||||
// it is legitimately possible for enough nodes to be Offline to prevent scheduling a shard.
|
|
||||||
tracing::info!("Scheduling failure, while excluding {hard_exclude:?}, node states:");
|
|
||||||
for (node_id, node) in &self.nodes {
|
|
||||||
tracing::info!(
|
|
||||||
"Node {node_id}: may_schedule={} shards={}",
|
|
||||||
node.may_schedule != MaySchedule::No,
|
|
||||||
node.shard_count
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err(ScheduleError::ImpossibleConstraint);
|
|
||||||
}
|
|
||||||
|
|
||||||
let node_id = tenant_counts.first().unwrap().0;
|
|
||||||
tracing::info!(
|
|
||||||
"scheduler selected node {node_id} (elegible nodes {:?}, exclude: {hard_exclude:?})",
|
|
||||||
tenant_counts.iter().map(|i| i.0 .0).collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note that we do not update shard count here to reflect the scheduling: that
|
|
||||||
// is IntentState's job when the scheduled location is used.
|
|
||||||
|
|
||||||
Ok(node_id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) mod test_utils {
|
|
||||||
|
|
||||||
use crate::node::Node;
|
|
||||||
use pageserver_api::controller_api::{NodeAvailability, UtilizationScore};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use utils::id::NodeId;
|
|
||||||
/// Test helper: synthesize the requested number of nodes, all in active state.
|
|
||||||
///
|
|
||||||
/// Node IDs start at one.
|
|
||||||
pub(crate) fn make_test_nodes(n: u64) -> HashMap<NodeId, Node> {
|
|
||||||
(1..n + 1)
|
|
||||||
.map(|i| {
|
|
||||||
(NodeId(i), {
|
|
||||||
let mut node = Node::new(
|
|
||||||
NodeId(i),
|
|
||||||
format!("httphost-{i}"),
|
|
||||||
80 + i as u16,
|
|
||||||
format!("pghost-{i}"),
|
|
||||||
5432 + i as u16,
|
|
||||||
);
|
|
||||||
node.set_availability(NodeAvailability::Active(UtilizationScore::worst()));
|
|
||||||
assert!(node.is_available());
|
|
||||||
node
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
use crate::tenant_state::IntentState;
|
|
||||||
#[test]
|
|
||||||
fn scheduler_basic() -> anyhow::Result<()> {
|
|
||||||
let nodes = test_utils::make_test_nodes(2);
|
|
||||||
|
|
||||||
let mut scheduler = Scheduler::new(nodes.values());
|
|
||||||
let mut t1_intent = IntentState::new();
|
|
||||||
let mut t2_intent = IntentState::new();
|
|
||||||
|
|
||||||
let scheduled = scheduler.schedule_shard(&[])?;
|
|
||||||
t1_intent.set_attached(&mut scheduler, Some(scheduled));
|
|
||||||
let scheduled = scheduler.schedule_shard(&[])?;
|
|
||||||
t2_intent.set_attached(&mut scheduler, Some(scheduled));
|
|
||||||
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 1);
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 1);
|
|
||||||
|
|
||||||
let scheduled = scheduler.schedule_shard(&t1_intent.all_pageservers())?;
|
|
||||||
t1_intent.push_secondary(&mut scheduler, scheduled);
|
|
||||||
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 1);
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 2);
|
|
||||||
|
|
||||||
t1_intent.clear(&mut scheduler);
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 0);
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 1);
|
|
||||||
|
|
||||||
if cfg!(debug_assertions) {
|
|
||||||
// Dropping an IntentState without clearing it causes a panic in debug mode,
|
|
||||||
// because we have failed to properly update scheduler shard counts.
|
|
||||||
let result = std::panic::catch_unwind(move || {
|
|
||||||
drop(t2_intent);
|
|
||||||
});
|
|
||||||
assert!(result.is_err());
|
|
||||||
} else {
|
|
||||||
t2_intent.clear(&mut scheduler);
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(1)).unwrap().shard_count, 0);
|
|
||||||
assert_eq!(scheduler.nodes.get(&NodeId(2)).unwrap().shard_count, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,983 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::{HashMap, HashSet},
|
|
||||||
sync::Arc,
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
metrics::{self, ReconcileCompleteLabelGroup, ReconcileOutcome},
|
|
||||||
persistence::TenantShardPersistence,
|
|
||||||
};
|
|
||||||
use pageserver_api::controller_api::PlacementPolicy;
|
|
||||||
use pageserver_api::{
|
|
||||||
models::{LocationConfig, LocationConfigMode, TenantConfig},
|
|
||||||
shard::{ShardIdentity, TenantShardId},
|
|
||||||
};
|
|
||||||
use serde::Serialize;
|
|
||||||
use tokio::task::JoinHandle;
|
|
||||||
use tokio_util::sync::CancellationToken;
|
|
||||||
use tracing::{instrument, Instrument};
|
|
||||||
use utils::{
|
|
||||||
generation::Generation,
|
|
||||||
id::NodeId,
|
|
||||||
seqwait::{SeqWait, SeqWaitError},
|
|
||||||
sync::gate::Gate,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
compute_hook::ComputeHook,
|
|
||||||
node::Node,
|
|
||||||
persistence::{split_state::SplitState, Persistence},
|
|
||||||
reconciler::{
|
|
||||||
attached_location_conf, secondary_location_conf, ReconcileError, Reconciler, TargetState,
|
|
||||||
},
|
|
||||||
scheduler::{ScheduleError, Scheduler},
|
|
||||||
service, Sequence,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Serialization helper
|
|
||||||
fn read_mutex_content<S, T>(v: &std::sync::Mutex<T>, serializer: S) -> Result<S::Ok, S::Error>
|
|
||||||
where
|
|
||||||
S: serde::ser::Serializer,
|
|
||||||
T: Clone + std::fmt::Display,
|
|
||||||
{
|
|
||||||
serializer.collect_str(&v.lock().unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// In-memory state for a particular tenant shard.
|
|
||||||
///
|
|
||||||
/// This struct implement Serialize for debugging purposes, but is _not_ persisted
|
|
||||||
/// itself: see [`crate::persistence`] for the subset of tenant shard state that is persisted.
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub(crate) struct TenantState {
|
|
||||||
pub(crate) tenant_shard_id: TenantShardId,
|
|
||||||
|
|
||||||
pub(crate) shard: ShardIdentity,
|
|
||||||
|
|
||||||
// Runtime only: sequence used to coordinate when updating this object while
|
|
||||||
// with background reconcilers may be running. A reconciler runs to a particular
|
|
||||||
// sequence.
|
|
||||||
pub(crate) sequence: Sequence,
|
|
||||||
|
|
||||||
// Latest generation number: next time we attach, increment this
|
|
||||||
// and use the incremented number when attaching.
|
|
||||||
//
|
|
||||||
// None represents an incompletely onboarded tenant via the [`Service::location_config`]
|
|
||||||
// API, where this tenant may only run in PlacementPolicy::Secondary.
|
|
||||||
pub(crate) generation: Option<Generation>,
|
|
||||||
|
|
||||||
// High level description of how the tenant should be set up. Provided
|
|
||||||
// externally.
|
|
||||||
pub(crate) policy: PlacementPolicy,
|
|
||||||
|
|
||||||
// Low level description of exactly which pageservers should fulfil
|
|
||||||
// which role. Generated by `Self::schedule`.
|
|
||||||
pub(crate) intent: IntentState,
|
|
||||||
|
|
||||||
// Low level description of how the tenant is configured on pageservers:
|
|
||||||
// if this does not match `Self::intent` then the tenant needs reconciliation
|
|
||||||
// with `Self::reconcile`.
|
|
||||||
pub(crate) observed: ObservedState,
|
|
||||||
|
|
||||||
// Tenant configuration, passed through opaquely to the pageserver. Identical
|
|
||||||
// for all shards in a tenant.
|
|
||||||
pub(crate) config: TenantConfig,
|
|
||||||
|
|
||||||
/// If a reconcile task is currently in flight, it may be joined here (it is
|
|
||||||
/// only safe to join if either the result has been received or the reconciler's
|
|
||||||
/// cancellation token has been fired)
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) reconciler: Option<ReconcilerHandle>,
|
|
||||||
|
|
||||||
/// If a tenant is being split, then all shards with that TenantId will have a
|
|
||||||
/// SplitState set, this acts as a guard against other operations such as background
|
|
||||||
/// reconciliation, and timeline creation.
|
|
||||||
pub(crate) splitting: SplitState,
|
|
||||||
|
|
||||||
/// Optionally wait for reconciliation to complete up to a particular
|
|
||||||
/// sequence number.
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) waiter: std::sync::Arc<SeqWait<Sequence, Sequence>>,
|
|
||||||
|
|
||||||
/// Indicates sequence number for which we have encountered an error reconciling. If
|
|
||||||
/// this advances ahead of [`Self::waiter`] then a reconciliation error has occurred,
|
|
||||||
/// and callers should stop waiting for `waiter` and propagate the error.
|
|
||||||
#[serde(skip)]
|
|
||||||
pub(crate) error_waiter: std::sync::Arc<SeqWait<Sequence, Sequence>>,
|
|
||||||
|
|
||||||
/// The most recent error from a reconcile on this tenant
|
|
||||||
/// TODO: generalize to an array of recent events
|
|
||||||
/// TOOD: use a ArcSwap instead of mutex for faster reads?
|
|
||||||
#[serde(serialize_with = "read_mutex_content")]
|
|
||||||
pub(crate) last_error: std::sync::Arc<std::sync::Mutex<String>>,
|
|
||||||
|
|
||||||
/// If we have a pending compute notification that for some reason we weren't able to send,
|
|
||||||
/// set this to true. If this is set, calls to [`Self::maybe_reconcile`] will run a task to retry
|
|
||||||
/// sending it. This is the mechanism by which compute notifications are included in the scope
|
|
||||||
/// of state that we publish externally in an eventually consistent way.
|
|
||||||
pub(crate) pending_compute_notification: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug, Serialize)]
|
|
||||||
pub(crate) struct IntentState {
|
|
||||||
attached: Option<NodeId>,
|
|
||||||
secondary: Vec<NodeId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntentState {
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
attached: None,
|
|
||||||
secondary: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub(crate) fn single(scheduler: &mut Scheduler, node_id: Option<NodeId>) -> Self {
|
|
||||||
if let Some(node_id) = node_id {
|
|
||||||
scheduler.node_inc_ref(node_id);
|
|
||||||
}
|
|
||||||
Self {
|
|
||||||
attached: node_id,
|
|
||||||
secondary: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn set_attached(&mut self, scheduler: &mut Scheduler, new_attached: Option<NodeId>) {
|
|
||||||
if self.attached != new_attached {
|
|
||||||
if let Some(old_attached) = self.attached.take() {
|
|
||||||
scheduler.node_dec_ref(old_attached);
|
|
||||||
}
|
|
||||||
if let Some(new_attached) = &new_attached {
|
|
||||||
scheduler.node_inc_ref(*new_attached);
|
|
||||||
}
|
|
||||||
self.attached = new_attached;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Like set_attached, but the node is from [`Self::secondary`]. This swaps the node from
|
|
||||||
/// secondary to attached while maintaining the scheduler's reference counts.
|
|
||||||
pub(crate) fn promote_attached(
|
|
||||||
&mut self,
|
|
||||||
_scheduler: &mut Scheduler,
|
|
||||||
promote_secondary: NodeId,
|
|
||||||
) {
|
|
||||||
// If we call this with a node that isn't in secondary, it would cause incorrect
|
|
||||||
// scheduler reference counting, since we assume the node is already referenced as a secondary.
|
|
||||||
debug_assert!(self.secondary.contains(&promote_secondary));
|
|
||||||
|
|
||||||
// TODO: when scheduler starts tracking attached + secondary counts separately, we will
|
|
||||||
// need to call into it here.
|
|
||||||
self.secondary.retain(|n| n != &promote_secondary);
|
|
||||||
self.attached = Some(promote_secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn push_secondary(&mut self, scheduler: &mut Scheduler, new_secondary: NodeId) {
|
|
||||||
debug_assert!(!self.secondary.contains(&new_secondary));
|
|
||||||
scheduler.node_inc_ref(new_secondary);
|
|
||||||
self.secondary.push(new_secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// It is legal to call this with a node that is not currently a secondary: that is a no-op
|
|
||||||
pub(crate) fn remove_secondary(&mut self, scheduler: &mut Scheduler, node_id: NodeId) {
|
|
||||||
let index = self.secondary.iter().position(|n| *n == node_id);
|
|
||||||
if let Some(index) = index {
|
|
||||||
scheduler.node_dec_ref(node_id);
|
|
||||||
self.secondary.remove(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn clear_secondary(&mut self, scheduler: &mut Scheduler) {
|
|
||||||
for secondary in self.secondary.drain(..) {
|
|
||||||
scheduler.node_dec_ref(secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Remove the last secondary node from the list of secondaries
|
|
||||||
pub(crate) fn pop_secondary(&mut self, scheduler: &mut Scheduler) {
|
|
||||||
if let Some(node_id) = self.secondary.pop() {
|
|
||||||
scheduler.node_dec_ref(node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn clear(&mut self, scheduler: &mut Scheduler) {
|
|
||||||
if let Some(old_attached) = self.attached.take() {
|
|
||||||
scheduler.node_dec_ref(old_attached);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.clear_secondary(scheduler);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn all_pageservers(&self) -> Vec<NodeId> {
|
|
||||||
let mut result = Vec::new();
|
|
||||||
if let Some(p) = self.attached {
|
|
||||||
result.push(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
result.extend(self.secondary.iter().copied());
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_attached(&self) -> &Option<NodeId> {
|
|
||||||
&self.attached
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_secondary(&self) -> &Vec<NodeId> {
|
|
||||||
&self.secondary
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If the node is in use as the attached location, demote it into
|
|
||||||
/// the list of secondary locations. This is used when a node goes offline,
|
|
||||||
/// and we want to use a different node for attachment, but not permanently
|
|
||||||
/// forget the location on the offline node.
|
|
||||||
///
|
|
||||||
/// Returns true if a change was made
|
|
||||||
pub(crate) fn demote_attached(&mut self, node_id: NodeId) -> bool {
|
|
||||||
if self.attached == Some(node_id) {
|
|
||||||
// TODO: when scheduler starts tracking attached + secondary counts separately, we will
|
|
||||||
// need to call into it here.
|
|
||||||
self.attached = None;
|
|
||||||
self.secondary.push(node_id);
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for IntentState {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// Must clear before dropping, to avoid leaving stale refcounts in the Scheduler
|
|
||||||
debug_assert!(self.attached.is_none() && self.secondary.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize)]
|
|
||||||
pub(crate) struct ObservedState {
|
|
||||||
pub(crate) locations: HashMap<NodeId, ObservedStateLocation>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Our latest knowledge of how this tenant is configured in the outside world.
|
|
||||||
///
|
|
||||||
/// Meaning:
|
|
||||||
/// * No instance of this type exists for a node: we are certain that we have nothing configured on that
|
|
||||||
/// node for this shard.
|
|
||||||
/// * Instance exists with conf==None: we *might* have some state on that node, but we don't know
|
|
||||||
/// what it is (e.g. we failed partway through configuring it)
|
|
||||||
/// * Instance exists with conf==Some: this tells us what we last successfully configured on this node,
|
|
||||||
/// and that configuration will still be present unless something external interfered.
|
|
||||||
#[derive(Clone, Serialize)]
|
|
||||||
pub(crate) struct ObservedStateLocation {
|
|
||||||
/// If None, it means we do not know the status of this shard's location on this node, but
|
|
||||||
/// we know that we might have some state on this node.
|
|
||||||
pub(crate) conf: Option<LocationConfig>,
|
|
||||||
}
|
|
||||||
pub(crate) struct ReconcilerWaiter {
|
|
||||||
// For observability purposes, remember the ID of the shard we're
|
|
||||||
// waiting for.
|
|
||||||
pub(crate) tenant_shard_id: TenantShardId,
|
|
||||||
|
|
||||||
seq_wait: std::sync::Arc<SeqWait<Sequence, Sequence>>,
|
|
||||||
error_seq_wait: std::sync::Arc<SeqWait<Sequence, Sequence>>,
|
|
||||||
error: std::sync::Arc<std::sync::Mutex<String>>,
|
|
||||||
seq: Sequence,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum ReconcileWaitError {
|
|
||||||
#[error("Timeout waiting for shard {0}")]
|
|
||||||
Timeout(TenantShardId),
|
|
||||||
#[error("shutting down")]
|
|
||||||
Shutdown,
|
|
||||||
#[error("Reconcile error on shard {0}: {1}")]
|
|
||||||
Failed(TenantShardId, String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ReconcilerWaiter {
|
|
||||||
pub(crate) async fn wait_timeout(&self, timeout: Duration) -> Result<(), ReconcileWaitError> {
|
|
||||||
tokio::select! {
|
|
||||||
result = self.seq_wait.wait_for_timeout(self.seq, timeout)=> {
|
|
||||||
result.map_err(|e| match e {
|
|
||||||
SeqWaitError::Timeout => ReconcileWaitError::Timeout(self.tenant_shard_id),
|
|
||||||
SeqWaitError::Shutdown => ReconcileWaitError::Shutdown
|
|
||||||
})?;
|
|
||||||
},
|
|
||||||
result = self.error_seq_wait.wait_for(self.seq) => {
|
|
||||||
result.map_err(|e| match e {
|
|
||||||
SeqWaitError::Shutdown => ReconcileWaitError::Shutdown,
|
|
||||||
SeqWaitError::Timeout => unreachable!()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
return Err(ReconcileWaitError::Failed(self.tenant_shard_id, self.error.lock().unwrap().clone()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Having spawned a reconciler task, the tenant shard's state will carry enough
|
|
||||||
/// information to optionally cancel & await it later.
|
|
||||||
pub(crate) struct ReconcilerHandle {
|
|
||||||
sequence: Sequence,
|
|
||||||
handle: JoinHandle<()>,
|
|
||||||
cancel: CancellationToken,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// When a reconcile task completes, it sends this result object
|
|
||||||
/// to be applied to the primary TenantState.
|
|
||||||
pub(crate) struct ReconcileResult {
|
|
||||||
pub(crate) sequence: Sequence,
|
|
||||||
/// On errors, `observed` should be treated as an incompleted description
|
|
||||||
/// of state (i.e. any nodes present in the result should override nodes
|
|
||||||
/// present in the parent tenant state, but any unmentioned nodes should
|
|
||||||
/// not be removed from parent tenant state)
|
|
||||||
pub(crate) result: Result<(), ReconcileError>,
|
|
||||||
|
|
||||||
pub(crate) tenant_shard_id: TenantShardId,
|
|
||||||
pub(crate) generation: Option<Generation>,
|
|
||||||
pub(crate) observed: ObservedState,
|
|
||||||
|
|
||||||
/// Set [`TenantState::pending_compute_notification`] from this flag
|
|
||||||
pub(crate) pending_compute_notification: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObservedState {
|
|
||||||
pub(crate) fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
locations: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TenantState {
|
|
||||||
pub(crate) fn new(
|
|
||||||
tenant_shard_id: TenantShardId,
|
|
||||||
shard: ShardIdentity,
|
|
||||||
policy: PlacementPolicy,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
tenant_shard_id,
|
|
||||||
policy,
|
|
||||||
intent: IntentState::default(),
|
|
||||||
generation: Some(Generation::new(0)),
|
|
||||||
shard,
|
|
||||||
observed: ObservedState::default(),
|
|
||||||
config: TenantConfig::default(),
|
|
||||||
reconciler: None,
|
|
||||||
splitting: SplitState::Idle,
|
|
||||||
sequence: Sequence(1),
|
|
||||||
waiter: Arc::new(SeqWait::new(Sequence(0))),
|
|
||||||
error_waiter: Arc::new(SeqWait::new(Sequence(0))),
|
|
||||||
last_error: Arc::default(),
|
|
||||||
pending_compute_notification: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// For use on startup when learning state from pageservers: generate my [`IntentState`] from my
|
|
||||||
/// [`ObservedState`], even if it violates my [`PlacementPolicy`]. Call [`Self::schedule`] next,
|
|
||||||
/// to get an intent state that complies with placement policy. The overall goal is to do scheduling
|
|
||||||
/// in a way that makes use of any configured locations that already exist in the outside world.
|
|
||||||
pub(crate) fn intent_from_observed(&mut self, scheduler: &mut Scheduler) {
|
|
||||||
// Choose an attached location by filtering observed locations, and then sorting to get the highest
|
|
||||||
// generation
|
|
||||||
let mut attached_locs = self
|
|
||||||
.observed
|
|
||||||
.locations
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(node_id, l)| {
|
|
||||||
if let Some(conf) = &l.conf {
|
|
||||||
if conf.mode == LocationConfigMode::AttachedMulti
|
|
||||||
|| conf.mode == LocationConfigMode::AttachedSingle
|
|
||||||
|| conf.mode == LocationConfigMode::AttachedStale
|
|
||||||
{
|
|
||||||
Some((node_id, conf.generation))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
attached_locs.sort_by_key(|i| i.1);
|
|
||||||
if let Some((node_id, _gen)) = attached_locs.into_iter().last() {
|
|
||||||
self.intent.set_attached(scheduler, Some(*node_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// All remaining observed locations generate secondary intents. This includes None
|
|
||||||
// observations, as these may well have some local content on disk that is usable (this
|
|
||||||
// is an edge case that might occur if we restarted during a migration or other change)
|
|
||||||
//
|
|
||||||
// We may leave intent.attached empty if we didn't find any attached locations: [`Self::schedule`]
|
|
||||||
// will take care of promoting one of these secondaries to be attached.
|
|
||||||
self.observed.locations.keys().for_each(|node_id| {
|
|
||||||
if Some(*node_id) != self.intent.attached {
|
|
||||||
self.intent.push_secondary(scheduler, *node_id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Part of [`Self::schedule`] that is used to choose exactly one node to act as the
|
|
||||||
/// attached pageserver for a shard.
|
|
||||||
///
|
|
||||||
/// Returns whether we modified it, and the NodeId selected.
|
|
||||||
fn schedule_attached(
|
|
||||||
&mut self,
|
|
||||||
scheduler: &mut Scheduler,
|
|
||||||
) -> Result<(bool, NodeId), ScheduleError> {
|
|
||||||
// No work to do if we already have an attached tenant
|
|
||||||
if let Some(node_id) = self.intent.attached {
|
|
||||||
return Ok((false, node_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(promote_secondary) = scheduler.node_preferred(&self.intent.secondary) {
|
|
||||||
// Promote a secondary
|
|
||||||
tracing::debug!("Promoted secondary {} to attached", promote_secondary);
|
|
||||||
self.intent.promote_attached(scheduler, promote_secondary);
|
|
||||||
Ok((true, promote_secondary))
|
|
||||||
} else {
|
|
||||||
// Pick a fresh node: either we had no secondaries or none were schedulable
|
|
||||||
let node_id = scheduler.schedule_shard(&self.intent.secondary)?;
|
|
||||||
tracing::debug!("Selected {} as attached", node_id);
|
|
||||||
self.intent.set_attached(scheduler, Some(node_id));
|
|
||||||
Ok((true, node_id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn schedule(&mut self, scheduler: &mut Scheduler) -> Result<(), ScheduleError> {
|
|
||||||
// TODO: before scheduling new nodes, check if any existing content in
|
|
||||||
// self.intent refers to pageservers that are offline, and pick other
|
|
||||||
// pageservers if so.
|
|
||||||
|
|
||||||
// TODO: respect the splitting bit on tenants: if they are currently splitting then we may not
|
|
||||||
// change their attach location.
|
|
||||||
|
|
||||||
// Build the set of pageservers already in use by this tenant, to avoid scheduling
|
|
||||||
// more work on the same pageservers we're already using.
|
|
||||||
let mut modified = false;
|
|
||||||
|
|
||||||
// Add/remove nodes to fulfil policy
|
|
||||||
use PlacementPolicy::*;
|
|
||||||
match self.policy {
|
|
||||||
Attached(secondary_count) => {
|
|
||||||
let retain_secondaries = if self.intent.attached.is_none()
|
|
||||||
&& scheduler.node_preferred(&self.intent.secondary).is_some()
|
|
||||||
{
|
|
||||||
// If we have no attached, and one of the secondaries is elegible to be promoted, retain
|
|
||||||
// one more secondary than we usually would, as one of them will become attached futher down this function.
|
|
||||||
secondary_count + 1
|
|
||||||
} else {
|
|
||||||
secondary_count
|
|
||||||
};
|
|
||||||
|
|
||||||
while self.intent.secondary.len() > retain_secondaries {
|
|
||||||
// We have no particular preference for one secondary location over another: just
|
|
||||||
// arbitrarily drop from the end
|
|
||||||
self.intent.pop_secondary(scheduler);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should have exactly one attached, and N secondaries
|
|
||||||
let (modified_attached, attached_node_id) = self.schedule_attached(scheduler)?;
|
|
||||||
modified |= modified_attached;
|
|
||||||
|
|
||||||
let mut used_pageservers = vec![attached_node_id];
|
|
||||||
while self.intent.secondary.len() < secondary_count {
|
|
||||||
let node_id = scheduler.schedule_shard(&used_pageservers)?;
|
|
||||||
self.intent.push_secondary(scheduler, node_id);
|
|
||||||
used_pageservers.push(node_id);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Secondary => {
|
|
||||||
if let Some(node_id) = self.intent.get_attached() {
|
|
||||||
// Populate secondary by demoting the attached node
|
|
||||||
self.intent.demote_attached(*node_id);
|
|
||||||
modified = true;
|
|
||||||
} else if self.intent.secondary.is_empty() {
|
|
||||||
// Populate secondary by scheduling a fresh node
|
|
||||||
let node_id = scheduler.schedule_shard(&[])?;
|
|
||||||
self.intent.push_secondary(scheduler, node_id);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
while self.intent.secondary.len() > 1 {
|
|
||||||
// We have no particular preference for one secondary location over another: just
|
|
||||||
// arbitrarily drop from the end
|
|
||||||
self.intent.pop_secondary(scheduler);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Detached => {
|
|
||||||
// Never add locations in this mode
|
|
||||||
if self.intent.get_attached().is_some() || !self.intent.get_secondary().is_empty() {
|
|
||||||
self.intent.clear(scheduler);
|
|
||||||
modified = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if modified {
|
|
||||||
self.sequence.0 += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Query whether the tenant's observed state for attached node matches its intent state, and if so,
|
|
||||||
/// yield the node ID. This is appropriate for emitting compute hook notifications: we are checking that
|
|
||||||
/// the node in question is not only where we intend to attach, but that the tenant is indeed already attached there.
|
|
||||||
///
|
|
||||||
/// Reconciliation may still be needed for other aspects of state such as secondaries (see [`Self::dirty`]): this
|
|
||||||
/// funciton should not be used to decide whether to reconcile.
|
|
||||||
pub(crate) fn stably_attached(&self) -> Option<NodeId> {
|
|
||||||
if let Some(attach_intent) = self.intent.attached {
|
|
||||||
match self.observed.locations.get(&attach_intent) {
|
|
||||||
Some(loc) => match &loc.conf {
|
|
||||||
Some(conf) => match conf.mode {
|
|
||||||
LocationConfigMode::AttachedMulti
|
|
||||||
| LocationConfigMode::AttachedSingle
|
|
||||||
| LocationConfigMode::AttachedStale => {
|
|
||||||
// Our intent and observed state agree that this node is in an attached state.
|
|
||||||
Some(attach_intent)
|
|
||||||
}
|
|
||||||
// Our observed config is not an attached state
|
|
||||||
_ => None,
|
|
||||||
},
|
|
||||||
// Our observed state is None, i.e. in flux
|
|
||||||
None => None,
|
|
||||||
},
|
|
||||||
// We have no observed state for this node
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Our intent is not to attach
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dirty(&self, nodes: &Arc<HashMap<NodeId, Node>>) -> bool {
|
|
||||||
let mut dirty_nodes = HashSet::new();
|
|
||||||
|
|
||||||
if let Some(node_id) = self.intent.attached {
|
|
||||||
// Maybe panic: it is a severe bug if we try to attach while generation is null.
|
|
||||||
let generation = self
|
|
||||||
.generation
|
|
||||||
.expect("Attempted to enter attached state without a generation");
|
|
||||||
|
|
||||||
let wanted_conf = attached_location_conf(
|
|
||||||
generation,
|
|
||||||
&self.shard,
|
|
||||||
&self.config,
|
|
||||||
!self.intent.secondary.is_empty(),
|
|
||||||
);
|
|
||||||
match self.observed.locations.get(&node_id) {
|
|
||||||
Some(conf) if conf.conf.as_ref() == Some(&wanted_conf) => {}
|
|
||||||
Some(_) | None => {
|
|
||||||
dirty_nodes.insert(node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for node_id in &self.intent.secondary {
|
|
||||||
let wanted_conf = secondary_location_conf(&self.shard, &self.config);
|
|
||||||
match self.observed.locations.get(node_id) {
|
|
||||||
Some(conf) if conf.conf.as_ref() == Some(&wanted_conf) => {}
|
|
||||||
Some(_) | None => {
|
|
||||||
dirty_nodes.insert(*node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for node_id in self.observed.locations.keys() {
|
|
||||||
if self.intent.attached != Some(*node_id) && !self.intent.secondary.contains(node_id) {
|
|
||||||
// We have observed state that isn't part of our intent: need to clean it up.
|
|
||||||
dirty_nodes.insert(*node_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dirty_nodes.retain(|node_id| {
|
|
||||||
nodes
|
|
||||||
.get(node_id)
|
|
||||||
.map(|n| n.is_available())
|
|
||||||
.unwrap_or(false)
|
|
||||||
});
|
|
||||||
|
|
||||||
!dirty_nodes.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
#[instrument(skip_all, fields(tenant_id=%self.tenant_shard_id.tenant_id, shard_id=%self.tenant_shard_id.shard_slug()))]
|
|
||||||
pub(crate) fn maybe_reconcile(
|
|
||||||
&mut self,
|
|
||||||
result_tx: &tokio::sync::mpsc::UnboundedSender<ReconcileResult>,
|
|
||||||
pageservers: &Arc<HashMap<NodeId, Node>>,
|
|
||||||
compute_hook: &Arc<ComputeHook>,
|
|
||||||
service_config: &service::Config,
|
|
||||||
persistence: &Arc<Persistence>,
|
|
||||||
gate: &Gate,
|
|
||||||
cancel: &CancellationToken,
|
|
||||||
) -> Option<ReconcilerWaiter> {
|
|
||||||
// If there are any ambiguous observed states, and the nodes they refer to are available,
|
|
||||||
// we should reconcile to clean them up.
|
|
||||||
let mut dirty_observed = false;
|
|
||||||
for (node_id, observed_loc) in &self.observed.locations {
|
|
||||||
let node = pageservers
|
|
||||||
.get(node_id)
|
|
||||||
.expect("Nodes may not be removed while referenced");
|
|
||||||
if observed_loc.conf.is_none() && node.is_available() {
|
|
||||||
dirty_observed = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let active_nodes_dirty = self.dirty(pageservers);
|
|
||||||
|
|
||||||
// Even if there is no pageserver work to be done, if we have a pending notification to computes,
|
|
||||||
// wake up a reconciler to send it.
|
|
||||||
let do_reconcile =
|
|
||||||
active_nodes_dirty || dirty_observed || self.pending_compute_notification;
|
|
||||||
|
|
||||||
if !do_reconcile {
|
|
||||||
tracing::info!("Not dirty, no reconciliation needed.");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we are currently splitting, then never start a reconciler task: the splitting logic
|
|
||||||
// requires that shards are not interfered with while it runs. Do this check here rather than
|
|
||||||
// up top, so that we only log this message if we would otherwise have done a reconciliation.
|
|
||||||
if !matches!(self.splitting, SplitState::Idle) {
|
|
||||||
tracing::info!("Refusing to reconcile, splitting in progress");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconcile already in flight for the current sequence?
|
|
||||||
if let Some(handle) = &self.reconciler {
|
|
||||||
if handle.sequence == self.sequence {
|
|
||||||
tracing::info!(
|
|
||||||
"Reconciliation already in progress for sequence {:?}",
|
|
||||||
self.sequence,
|
|
||||||
);
|
|
||||||
return Some(ReconcilerWaiter {
|
|
||||||
tenant_shard_id: self.tenant_shard_id,
|
|
||||||
seq_wait: self.waiter.clone(),
|
|
||||||
error_seq_wait: self.error_waiter.clone(),
|
|
||||||
error: self.last_error.clone(),
|
|
||||||
seq: self.sequence,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build list of nodes from which the reconciler should detach
|
|
||||||
let mut detach = Vec::new();
|
|
||||||
for node_id in self.observed.locations.keys() {
|
|
||||||
if self.intent.get_attached() != &Some(*node_id)
|
|
||||||
&& !self.intent.secondary.contains(node_id)
|
|
||||||
{
|
|
||||||
detach.push(
|
|
||||||
pageservers
|
|
||||||
.get(node_id)
|
|
||||||
.expect("Intent references non-existent pageserver")
|
|
||||||
.clone(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconcile in flight for a stale sequence? Our sequence's task will wait for it before
|
|
||||||
// doing our sequence's work.
|
|
||||||
let old_handle = self.reconciler.take();
|
|
||||||
|
|
||||||
let Ok(gate_guard) = gate.enter() else {
|
|
||||||
// Shutting down, don't start a reconciler
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Advance the sequence before spawning a reconciler, so that sequence waiters
|
|
||||||
// can distinguish between before+after the reconcile completes.
|
|
||||||
self.sequence = self.sequence.next();
|
|
||||||
|
|
||||||
let reconciler_cancel = cancel.child_token();
|
|
||||||
let reconciler_intent = TargetState::from_intent(pageservers, &self.intent);
|
|
||||||
let mut reconciler = Reconciler {
|
|
||||||
tenant_shard_id: self.tenant_shard_id,
|
|
||||||
shard: self.shard,
|
|
||||||
generation: self.generation,
|
|
||||||
intent: reconciler_intent,
|
|
||||||
detach,
|
|
||||||
config: self.config.clone(),
|
|
||||||
observed: self.observed.clone(),
|
|
||||||
compute_hook: compute_hook.clone(),
|
|
||||||
service_config: service_config.clone(),
|
|
||||||
_gate_guard: gate_guard,
|
|
||||||
cancel: reconciler_cancel.clone(),
|
|
||||||
persistence: persistence.clone(),
|
|
||||||
compute_notify_failure: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
let reconcile_seq = self.sequence;
|
|
||||||
|
|
||||||
tracing::info!(seq=%reconcile_seq, "Spawning Reconciler for sequence {}", self.sequence);
|
|
||||||
let must_notify = self.pending_compute_notification;
|
|
||||||
let reconciler_span = tracing::info_span!(parent: None, "reconciler", seq=%reconcile_seq,
|
|
||||||
tenant_id=%reconciler.tenant_shard_id.tenant_id,
|
|
||||||
shard_id=%reconciler.tenant_shard_id.shard_slug());
|
|
||||||
metrics::METRICS_REGISTRY
|
|
||||||
.metrics_group
|
|
||||||
.storage_controller_reconcile_spawn
|
|
||||||
.inc();
|
|
||||||
let result_tx = result_tx.clone();
|
|
||||||
let join_handle = tokio::task::spawn(
|
|
||||||
async move {
|
|
||||||
// Wait for any previous reconcile task to complete before we start
|
|
||||||
if let Some(old_handle) = old_handle {
|
|
||||||
old_handle.cancel.cancel();
|
|
||||||
if let Err(e) = old_handle.handle.await {
|
|
||||||
// We can't do much with this other than log it: the task is done, so
|
|
||||||
// we may proceed with our work.
|
|
||||||
tracing::error!("Unexpected join error waiting for reconcile task: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Early check for cancellation before doing any work
|
|
||||||
// TODO: wrap all remote API operations in cancellation check
|
|
||||||
// as well.
|
|
||||||
if reconciler.cancel.is_cancelled() {
|
|
||||||
metrics::METRICS_REGISTRY
|
|
||||||
.metrics_group
|
|
||||||
.storage_controller_reconcile_complete
|
|
||||||
.inc(ReconcileCompleteLabelGroup {
|
|
||||||
status: ReconcileOutcome::Cancel,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to make observed state match intent state
|
|
||||||
let result = reconciler.reconcile().await;
|
|
||||||
|
|
||||||
// If we know we had a pending compute notification from some previous action, send a notification irrespective
|
|
||||||
// of whether the above reconcile() did any work
|
|
||||||
if result.is_ok() && must_notify {
|
|
||||||
// If this fails we will send the need to retry in [`ReconcileResult::pending_compute_notification`]
|
|
||||||
reconciler.compute_notify().await.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update result counter
|
|
||||||
let outcome_label = match &result {
|
|
||||||
Ok(_) => ReconcileOutcome::Success,
|
|
||||||
Err(ReconcileError::Cancel) => ReconcileOutcome::Cancel,
|
|
||||||
Err(_) => ReconcileOutcome::Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
metrics::METRICS_REGISTRY
|
|
||||||
.metrics_group
|
|
||||||
.storage_controller_reconcile_complete
|
|
||||||
.inc(ReconcileCompleteLabelGroup {
|
|
||||||
status: outcome_label,
|
|
||||||
});
|
|
||||||
|
|
||||||
result_tx
|
|
||||||
.send(ReconcileResult {
|
|
||||||
sequence: reconcile_seq,
|
|
||||||
result,
|
|
||||||
tenant_shard_id: reconciler.tenant_shard_id,
|
|
||||||
generation: reconciler.generation,
|
|
||||||
observed: reconciler.observed,
|
|
||||||
pending_compute_notification: reconciler.compute_notify_failure,
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
.instrument(reconciler_span),
|
|
||||||
);
|
|
||||||
|
|
||||||
self.reconciler = Some(ReconcilerHandle {
|
|
||||||
sequence: self.sequence,
|
|
||||||
handle: join_handle,
|
|
||||||
cancel: reconciler_cancel,
|
|
||||||
});
|
|
||||||
|
|
||||||
Some(ReconcilerWaiter {
|
|
||||||
tenant_shard_id: self.tenant_shard_id,
|
|
||||||
seq_wait: self.waiter.clone(),
|
|
||||||
error_seq_wait: self.error_waiter.clone(),
|
|
||||||
error: self.last_error.clone(),
|
|
||||||
seq: self.sequence,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called when a ReconcileResult has been emitted and the service is updating
|
|
||||||
/// our state: if the result is from a sequence >= my ReconcileHandle, then drop
|
|
||||||
/// the handle to indicate there is no longer a reconciliation in progress.
|
|
||||||
pub(crate) fn reconcile_complete(&mut self, sequence: Sequence) {
|
|
||||||
if let Some(reconcile_handle) = &self.reconciler {
|
|
||||||
if reconcile_handle.sequence <= sequence {
|
|
||||||
self.reconciler = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we had any state at all referring to this node ID, drop it. Does not
|
|
||||||
// attempt to reschedule.
|
|
||||||
pub(crate) fn deref_node(&mut self, node_id: NodeId) {
|
|
||||||
if self.intent.attached == Some(node_id) {
|
|
||||||
self.intent.attached = None;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.intent.secondary.retain(|n| n != &node_id);
|
|
||||||
|
|
||||||
self.observed.locations.remove(&node_id);
|
|
||||||
|
|
||||||
debug_assert!(!self.intent.all_pageservers().contains(&node_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn to_persistent(&self) -> TenantShardPersistence {
|
|
||||||
TenantShardPersistence {
|
|
||||||
tenant_id: self.tenant_shard_id.tenant_id.to_string(),
|
|
||||||
shard_number: self.tenant_shard_id.shard_number.0 as i32,
|
|
||||||
shard_count: self.tenant_shard_id.shard_count.literal() as i32,
|
|
||||||
shard_stripe_size: self.shard.stripe_size.0 as i32,
|
|
||||||
generation: self.generation.map(|g| g.into().unwrap_or(0) as i32),
|
|
||||||
generation_pageserver: self.intent.get_attached().map(|n| n.0 as i64),
|
|
||||||
placement_policy: serde_json::to_string(&self.policy).unwrap(),
|
|
||||||
config: serde_json::to_string(&self.config).unwrap(),
|
|
||||||
splitting: SplitState::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) mod tests {
|
|
||||||
use pageserver_api::{
|
|
||||||
controller_api::NodeAvailability,
|
|
||||||
shard::{ShardCount, ShardNumber},
|
|
||||||
};
|
|
||||||
use utils::id::TenantId;
|
|
||||||
|
|
||||||
use crate::scheduler::test_utils::make_test_nodes;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn make_test_tenant_shard(policy: PlacementPolicy) -> TenantState {
|
|
||||||
let tenant_id = TenantId::generate();
|
|
||||||
let shard_number = ShardNumber(0);
|
|
||||||
let shard_count = ShardCount::new(1);
|
|
||||||
|
|
||||||
let tenant_shard_id = TenantShardId {
|
|
||||||
tenant_id,
|
|
||||||
shard_number,
|
|
||||||
shard_count,
|
|
||||||
};
|
|
||||||
TenantState::new(
|
|
||||||
tenant_shard_id,
|
|
||||||
ShardIdentity::new(
|
|
||||||
shard_number,
|
|
||||||
shard_count,
|
|
||||||
pageserver_api::shard::ShardStripeSize(32768),
|
|
||||||
)
|
|
||||||
.unwrap(),
|
|
||||||
policy,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test the scheduling behaviors used when a tenant configured for HA is subject
|
|
||||||
/// to nodes being marked offline.
|
|
||||||
#[test]
|
|
||||||
fn tenant_ha_scheduling() -> anyhow::Result<()> {
|
|
||||||
// Start with three nodes. Our tenant will only use two. The third one is
|
|
||||||
// expected to remain unused.
|
|
||||||
let mut nodes = make_test_nodes(3);
|
|
||||||
|
|
||||||
let mut scheduler = Scheduler::new(nodes.values());
|
|
||||||
|
|
||||||
let mut tenant_state = make_test_tenant_shard(PlacementPolicy::Attached(1));
|
|
||||||
tenant_state
|
|
||||||
.schedule(&mut scheduler)
|
|
||||||
.expect("we have enough nodes, scheduling should work");
|
|
||||||
|
|
||||||
// Expect to initially be schedule on to different nodes
|
|
||||||
assert_eq!(tenant_state.intent.secondary.len(), 1);
|
|
||||||
assert!(tenant_state.intent.attached.is_some());
|
|
||||||
|
|
||||||
let attached_node_id = tenant_state.intent.attached.unwrap();
|
|
||||||
let secondary_node_id = *tenant_state.intent.secondary.iter().last().unwrap();
|
|
||||||
assert_ne!(attached_node_id, secondary_node_id);
|
|
||||||
|
|
||||||
// Notifying the attached node is offline should demote it to a secondary
|
|
||||||
let changed = tenant_state.intent.demote_attached(attached_node_id);
|
|
||||||
assert!(changed);
|
|
||||||
assert!(tenant_state.intent.attached.is_none());
|
|
||||||
assert_eq!(tenant_state.intent.secondary.len(), 2);
|
|
||||||
|
|
||||||
// Update the scheduler state to indicate the node is offline
|
|
||||||
nodes
|
|
||||||
.get_mut(&attached_node_id)
|
|
||||||
.unwrap()
|
|
||||||
.set_availability(NodeAvailability::Offline);
|
|
||||||
scheduler.node_upsert(nodes.get(&attached_node_id).unwrap());
|
|
||||||
|
|
||||||
// Scheduling the node should promote the still-available secondary node to attached
|
|
||||||
tenant_state
|
|
||||||
.schedule(&mut scheduler)
|
|
||||||
.expect("active nodes are available");
|
|
||||||
assert_eq!(tenant_state.intent.attached.unwrap(), secondary_node_id);
|
|
||||||
|
|
||||||
// The original attached node should have been retained as a secondary
|
|
||||||
assert_eq!(
|
|
||||||
*tenant_state.intent.secondary.iter().last().unwrap(),
|
|
||||||
attached_node_id
|
|
||||||
);
|
|
||||||
|
|
||||||
tenant_state.intent.clear(&mut scheduler);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn intent_from_observed() -> anyhow::Result<()> {
|
|
||||||
let nodes = make_test_nodes(3);
|
|
||||||
let mut scheduler = Scheduler::new(nodes.values());
|
|
||||||
|
|
||||||
let mut tenant_state = make_test_tenant_shard(PlacementPolicy::Attached(1));
|
|
||||||
|
|
||||||
tenant_state.observed.locations.insert(
|
|
||||||
NodeId(3),
|
|
||||||
ObservedStateLocation {
|
|
||||||
conf: Some(LocationConfig {
|
|
||||||
mode: LocationConfigMode::AttachedMulti,
|
|
||||||
generation: Some(2),
|
|
||||||
secondary_conf: None,
|
|
||||||
shard_number: tenant_state.shard.number.0,
|
|
||||||
shard_count: tenant_state.shard.count.literal(),
|
|
||||||
shard_stripe_size: tenant_state.shard.stripe_size.0,
|
|
||||||
tenant_conf: TenantConfig::default(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tenant_state.observed.locations.insert(
|
|
||||||
NodeId(2),
|
|
||||||
ObservedStateLocation {
|
|
||||||
conf: Some(LocationConfig {
|
|
||||||
mode: LocationConfigMode::AttachedStale,
|
|
||||||
generation: Some(1),
|
|
||||||
secondary_conf: None,
|
|
||||||
shard_number: tenant_state.shard.number.0,
|
|
||||||
shard_count: tenant_state.shard.count.literal(),
|
|
||||||
shard_stripe_size: tenant_state.shard.stripe_size.0,
|
|
||||||
tenant_conf: TenantConfig::default(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
tenant_state.intent_from_observed(&mut scheduler);
|
|
||||||
|
|
||||||
// The highest generationed attached location gets used as attached
|
|
||||||
assert_eq!(tenant_state.intent.attached, Some(NodeId(3)));
|
|
||||||
// Other locations get used as secondary
|
|
||||||
assert_eq!(tenant_state.intent.secondary, vec![NodeId(2)]);
|
|
||||||
|
|
||||||
scheduler.consistency_check(nodes.values(), [&tenant_state].into_iter())?;
|
|
||||||
|
|
||||||
tenant_state.intent.clear(&mut scheduler);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,11 +36,11 @@ use utils::pid_file::{self, PidFileRead};
|
|||||||
// it's waiting. If the process hasn't started/stopped after 5 seconds,
|
// it's waiting. If the process hasn't started/stopped after 5 seconds,
|
||||||
// it prints a notice that it's taking long, but keeps waiting.
|
// it prints a notice that it's taking long, but keeps waiting.
|
||||||
//
|
//
|
||||||
const RETRY_UNTIL_SECS: u64 = 10;
|
const STOP_RETRY_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
const RETRIES: u64 = (RETRY_UNTIL_SECS * 1000) / RETRY_INTERVAL_MILLIS;
|
const STOP_RETRIES: u128 = STOP_RETRY_TIMEOUT.as_millis() / RETRY_INTERVAL.as_millis();
|
||||||
const RETRY_INTERVAL_MILLIS: u64 = 100;
|
const RETRY_INTERVAL: Duration = Duration::from_millis(100);
|
||||||
const DOT_EVERY_RETRIES: u64 = 10;
|
const DOT_EVERY_RETRIES: u128 = 10;
|
||||||
const NOTICE_AFTER_RETRIES: u64 = 50;
|
const NOTICE_AFTER_RETRIES: u128 = 50;
|
||||||
|
|
||||||
/// Argument to `start_process`, to indicate whether it should create pidfile or if the process creates
|
/// Argument to `start_process`, to indicate whether it should create pidfile or if the process creates
|
||||||
/// it itself.
|
/// it itself.
|
||||||
@@ -52,6 +52,7 @@ pub enum InitialPidFile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Start a background child process using the parameters given.
|
/// Start a background child process using the parameters given.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub async fn start_process<F, Fut, AI, A, EI>(
|
pub async fn start_process<F, Fut, AI, A, EI>(
|
||||||
process_name: &str,
|
process_name: &str,
|
||||||
datadir: &Path,
|
datadir: &Path,
|
||||||
@@ -59,6 +60,7 @@ pub async fn start_process<F, Fut, AI, A, EI>(
|
|||||||
args: AI,
|
args: AI,
|
||||||
envs: EI,
|
envs: EI,
|
||||||
initial_pid_file: InitialPidFile,
|
initial_pid_file: InitialPidFile,
|
||||||
|
retry_timeout: &Duration,
|
||||||
process_status_check: F,
|
process_status_check: F,
|
||||||
) -> anyhow::Result<()>
|
) -> anyhow::Result<()>
|
||||||
where
|
where
|
||||||
@@ -69,6 +71,10 @@ where
|
|||||||
// Not generic AsRef<OsStr>, otherwise empty `envs` prevents type inference
|
// Not generic AsRef<OsStr>, otherwise empty `envs` prevents type inference
|
||||||
EI: IntoIterator<Item = (String, String)>,
|
EI: IntoIterator<Item = (String, String)>,
|
||||||
{
|
{
|
||||||
|
let retries: u128 = retry_timeout.as_millis() / RETRY_INTERVAL.as_millis();
|
||||||
|
if !datadir.metadata().context("stat datadir")?.is_dir() {
|
||||||
|
anyhow::bail!("`datadir` must be a directory when calling this function: {datadir:?}");
|
||||||
|
}
|
||||||
let log_path = datadir.join(format!("{process_name}.log"));
|
let log_path = datadir.join(format!("{process_name}.log"));
|
||||||
let process_log_file = fs::OpenOptions::new()
|
let process_log_file = fs::OpenOptions::new()
|
||||||
.create(true)
|
.create(true)
|
||||||
@@ -85,8 +91,17 @@ where
|
|||||||
let background_command = command
|
let background_command = command
|
||||||
.stdout(process_log_file)
|
.stdout(process_log_file)
|
||||||
.stderr(same_file_for_stderr)
|
.stderr(same_file_for_stderr)
|
||||||
.args(args);
|
.args(args)
|
||||||
let filled_cmd = fill_remote_storage_secrets_vars(fill_rust_env_vars(background_command));
|
// spawn all child processes in their datadir, useful for all kinds of things,
|
||||||
|
// not least cleaning up child processes e.g. after an unclean exit from the test suite:
|
||||||
|
// ```
|
||||||
|
// lsof -d cwd -a +D Users/cs/src/neon/test_output
|
||||||
|
// ```
|
||||||
|
.current_dir(datadir);
|
||||||
|
|
||||||
|
let filled_cmd = fill_env_vars_prefixed_neon(fill_remote_storage_secrets_vars(
|
||||||
|
fill_rust_env_vars(background_command),
|
||||||
|
));
|
||||||
filled_cmd.envs(envs);
|
filled_cmd.envs(envs);
|
||||||
|
|
||||||
let pid_file_to_check = match &initial_pid_file {
|
let pid_file_to_check = match &initial_pid_file {
|
||||||
@@ -118,7 +133,7 @@ where
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
});
|
});
|
||||||
|
|
||||||
for retries in 0..RETRIES {
|
for retries in 0..retries {
|
||||||
match process_started(pid, pid_file_to_check, &process_status_check).await {
|
match process_started(pid, pid_file_to_check, &process_status_check).await {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
println!("\n{process_name} started and passed status check, pid: {pid}");
|
println!("\n{process_name} started and passed status check, pid: {pid}");
|
||||||
@@ -136,7 +151,7 @@ where
|
|||||||
print!(".");
|
print!(".");
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(RETRY_INTERVAL_MILLIS));
|
thread::sleep(RETRY_INTERVAL);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("error starting process {process_name:?}: {e:#}");
|
println!("error starting process {process_name:?}: {e:#}");
|
||||||
@@ -145,9 +160,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
anyhow::bail!(
|
anyhow::bail!(format!(
|
||||||
"{process_name} did not start+pass status checks within {RETRY_UNTIL_SECS} seconds"
|
"{} did not start+pass status checks within {:?} seconds",
|
||||||
);
|
process_name, retry_timeout
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stops the process, using the pid file given. Returns Ok also if the process is already not running.
|
/// Stops the process, using the pid file given. Returns Ok also if the process is already not running.
|
||||||
@@ -203,7 +219,7 @@ pub fn stop_process(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn wait_until_stopped(process_name: &str, pid: Pid) -> anyhow::Result<()> {
|
pub fn wait_until_stopped(process_name: &str, pid: Pid) -> anyhow::Result<()> {
|
||||||
for retries in 0..RETRIES {
|
for retries in 0..STOP_RETRIES {
|
||||||
match process_has_stopped(pid) {
|
match process_has_stopped(pid) {
|
||||||
Ok(true) => {
|
Ok(true) => {
|
||||||
println!("\n{process_name} stopped");
|
println!("\n{process_name} stopped");
|
||||||
@@ -219,7 +235,7 @@ pub fn wait_until_stopped(process_name: &str, pid: Pid) -> anyhow::Result<()> {
|
|||||||
print!(".");
|
print!(".");
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(RETRY_INTERVAL_MILLIS));
|
thread::sleep(RETRY_INTERVAL);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
println!("{process_name} with pid {pid} failed to stop: {e:#}");
|
println!("{process_name} with pid {pid} failed to stop: {e:#}");
|
||||||
@@ -228,7 +244,10 @@ pub fn wait_until_stopped(process_name: &str, pid: Pid) -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!();
|
println!();
|
||||||
anyhow::bail!("{process_name} with pid {pid} did not stop in {RETRY_UNTIL_SECS} seconds");
|
anyhow::bail!(format!(
|
||||||
|
"{} with pid {} did not stop in {:?} seconds",
|
||||||
|
process_name, pid, STOP_RETRY_TIMEOUT
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_rust_env_vars(cmd: &mut Command) -> &mut Command {
|
fn fill_rust_env_vars(cmd: &mut Command) -> &mut Command {
|
||||||
@@ -268,6 +287,15 @@ fn fill_remote_storage_secrets_vars(mut cmd: &mut Command) -> &mut Command {
|
|||||||
cmd
|
cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fill_env_vars_prefixed_neon(mut cmd: &mut Command) -> &mut Command {
|
||||||
|
for (var, val) in std::env::vars() {
|
||||||
|
if var.starts_with("NEON_PAGESERVER_") {
|
||||||
|
cmd = cmd.env(var, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a `pre_exec` to the cmd that, inbetween fork() and exec(),
|
/// Add a `pre_exec` to the cmd that, inbetween fork() and exec(),
|
||||||
/// 1. Claims a pidfile with a fcntl lock on it and
|
/// 1. Claims a pidfile with a fcntl lock on it and
|
||||||
/// 2. Sets up the pidfile's file descriptor so that it (and the lock)
|
/// 2. Sets up the pidfile's file descriptor so that it (and the lock)
|
||||||
|
|||||||
@@ -9,22 +9,23 @@ use anyhow::{anyhow, bail, Context, Result};
|
|||||||
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command, ValueEnum};
|
use clap::{value_parser, Arg, ArgAction, ArgMatches, Command, ValueEnum};
|
||||||
use compute_api::spec::ComputeMode;
|
use compute_api::spec::ComputeMode;
|
||||||
use control_plane::endpoint::ComputeControlPlane;
|
use control_plane::endpoint::ComputeControlPlane;
|
||||||
use control_plane::local_env::{InitForceMode, LocalEnv};
|
use control_plane::local_env::{
|
||||||
use control_plane::pageserver::{PageServerNode, PAGESERVER_REMOTE_STORAGE_DIR};
|
InitForceMode, LocalEnv, NeonBroker, NeonLocalInitConf, NeonLocalInitPageserverConf,
|
||||||
|
SafekeeperConf,
|
||||||
|
};
|
||||||
|
use control_plane::pageserver::PageServerNode;
|
||||||
use control_plane::safekeeper::SafekeeperNode;
|
use control_plane::safekeeper::SafekeeperNode;
|
||||||
use control_plane::storage_controller::StorageController;
|
use control_plane::storage_controller::StorageController;
|
||||||
use control_plane::{broker, local_env};
|
use control_plane::{broker, local_env};
|
||||||
use pageserver_api::controller_api::{
|
use pageserver_api::config::{
|
||||||
NodeAvailability, NodeConfigureRequest, NodeSchedulingPolicy, PlacementPolicy,
|
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_PAGESERVER_HTTP_PORT,
|
||||||
|
DEFAULT_PG_LISTEN_PORT as DEFAULT_PAGESERVER_PG_PORT,
|
||||||
};
|
};
|
||||||
|
use pageserver_api::controller_api::PlacementPolicy;
|
||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
ShardParameters, TenantCreateRequest, TimelineCreateRequest, TimelineInfo,
|
ShardParameters, TenantCreateRequest, TimelineCreateRequest, TimelineInfo,
|
||||||
};
|
};
|
||||||
use pageserver_api::shard::{ShardCount, ShardStripeSize, TenantShardId};
|
use pageserver_api::shard::{ShardCount, ShardStripeSize, TenantShardId};
|
||||||
use pageserver_api::{
|
|
||||||
DEFAULT_HTTP_LISTEN_PORT as DEFAULT_PAGESERVER_HTTP_PORT,
|
|
||||||
DEFAULT_PG_LISTEN_PORT as DEFAULT_PAGESERVER_PG_PORT,
|
|
||||||
};
|
|
||||||
use postgres_backend::AuthType;
|
use postgres_backend::AuthType;
|
||||||
use postgres_connection::parse_host_port;
|
use postgres_connection::parse_host_port;
|
||||||
use safekeeper_api::{
|
use safekeeper_api::{
|
||||||
@@ -35,6 +36,7 @@ use std::collections::{BTreeSet, HashMap};
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::time::Duration;
|
||||||
use storage_broker::DEFAULT_LISTEN_ADDR as DEFAULT_BROKER_ADDR;
|
use storage_broker::DEFAULT_LISTEN_ADDR as DEFAULT_BROKER_ADDR;
|
||||||
use url::Host;
|
use url::Host;
|
||||||
use utils::{
|
use utils::{
|
||||||
@@ -54,44 +56,6 @@ const DEFAULT_PG_VERSION: &str = "15";
|
|||||||
|
|
||||||
const DEFAULT_PAGESERVER_CONTROL_PLANE_API: &str = "http://127.0.0.1:1234/upcall/v1/";
|
const DEFAULT_PAGESERVER_CONTROL_PLANE_API: &str = "http://127.0.0.1:1234/upcall/v1/";
|
||||||
|
|
||||||
fn default_conf(num_pageservers: u16) -> String {
|
|
||||||
let mut template = format!(
|
|
||||||
r#"
|
|
||||||
# Default built-in configuration, defined in main.rs
|
|
||||||
control_plane_api = '{DEFAULT_PAGESERVER_CONTROL_PLANE_API}'
|
|
||||||
|
|
||||||
[broker]
|
|
||||||
listen_addr = '{DEFAULT_BROKER_ADDR}'
|
|
||||||
|
|
||||||
[[safekeepers]]
|
|
||||||
id = {DEFAULT_SAFEKEEPER_ID}
|
|
||||||
pg_port = {DEFAULT_SAFEKEEPER_PG_PORT}
|
|
||||||
http_port = {DEFAULT_SAFEKEEPER_HTTP_PORT}
|
|
||||||
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
for i in 0..num_pageservers {
|
|
||||||
let pageserver_id = NodeId(DEFAULT_PAGESERVER_ID.0 + i as u64);
|
|
||||||
let pg_port = DEFAULT_PAGESERVER_PG_PORT + i;
|
|
||||||
let http_port = DEFAULT_PAGESERVER_HTTP_PORT + i;
|
|
||||||
|
|
||||||
template += &format!(
|
|
||||||
r#"
|
|
||||||
[[pageservers]]
|
|
||||||
id = {pageserver_id}
|
|
||||||
listen_pg_addr = '127.0.0.1:{pg_port}'
|
|
||||||
listen_http_addr = '127.0.0.1:{http_port}'
|
|
||||||
pg_auth_type = '{trust_auth}'
|
|
||||||
http_auth_type = '{trust_auth}'
|
|
||||||
"#,
|
|
||||||
trust_auth = AuthType::Trust,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
template
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Timelines tree element used as a value in the HashMap.
|
/// Timelines tree element used as a value in the HashMap.
|
||||||
///
|
///
|
||||||
@@ -124,7 +88,8 @@ fn main() -> Result<()> {
|
|||||||
handle_init(sub_args).map(Some)
|
handle_init(sub_args).map(Some)
|
||||||
} else {
|
} else {
|
||||||
// all other commands need an existing config
|
// all other commands need an existing config
|
||||||
let mut env = LocalEnv::load_config().context("Error loading config")?;
|
let mut env =
|
||||||
|
LocalEnv::load_config(&local_env::base_path()).context("Error loading config")?;
|
||||||
let original_env = env.clone();
|
let original_env = env.clone();
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
@@ -135,7 +100,7 @@ fn main() -> Result<()> {
|
|||||||
let subcommand_result = match sub_name {
|
let subcommand_result = match sub_name {
|
||||||
"tenant" => rt.block_on(handle_tenant(sub_args, &mut env)),
|
"tenant" => rt.block_on(handle_tenant(sub_args, &mut env)),
|
||||||
"timeline" => rt.block_on(handle_timeline(sub_args, &mut env)),
|
"timeline" => rt.block_on(handle_timeline(sub_args, &mut env)),
|
||||||
"start" => rt.block_on(handle_start_all(sub_args, &env)),
|
"start" => rt.block_on(handle_start_all(&env, get_start_timeout(sub_args))),
|
||||||
"stop" => rt.block_on(handle_stop_all(sub_args, &env)),
|
"stop" => rt.block_on(handle_stop_all(sub_args, &env)),
|
||||||
"pageserver" => rt.block_on(handle_pageserver(sub_args, &env)),
|
"pageserver" => rt.block_on(handle_pageserver(sub_args, &env)),
|
||||||
"storage_controller" => rt.block_on(handle_storage_controller(sub_args, &env)),
|
"storage_controller" => rt.block_on(handle_storage_controller(sub_args, &env)),
|
||||||
@@ -154,7 +119,7 @@ fn main() -> Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
match subcommand_result {
|
match subcommand_result {
|
||||||
Ok(Some(updated_env)) => updated_env.persist_config(&updated_env.base_data_dir)?,
|
Ok(Some(updated_env)) => updated_env.persist_config()?,
|
||||||
Ok(None) => (),
|
Ok(None) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("command failed: {e:?}");
|
eprintln!("command failed: {e:?}");
|
||||||
@@ -343,48 +308,66 @@ fn parse_timeline_id(sub_match: &ArgMatches) -> anyhow::Result<Option<TimelineId
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_init(init_match: &ArgMatches) -> anyhow::Result<LocalEnv> {
|
fn handle_init(init_match: &ArgMatches) -> anyhow::Result<LocalEnv> {
|
||||||
let num_pageservers = init_match
|
let num_pageservers = init_match.get_one::<u16>("num-pageservers");
|
||||||
.get_one::<u16>("num-pageservers")
|
|
||||||
.expect("num-pageservers arg has a default");
|
let force = init_match.get_one("force").expect("we set a default value");
|
||||||
// Create config file
|
|
||||||
let toml_file: String = if let Some(config_path) = init_match.get_one::<PathBuf>("config") {
|
// Create the in-memory `LocalEnv` that we'd normally load from disk in `load_config`.
|
||||||
|
let init_conf: NeonLocalInitConf = if let Some(config_path) =
|
||||||
|
init_match.get_one::<PathBuf>("config")
|
||||||
|
{
|
||||||
|
// User (likely the Python test suite) provided a description of the environment.
|
||||||
|
if num_pageservers.is_some() {
|
||||||
|
bail!("Cannot specify both --num-pageservers and --config, use key `pageservers` in the --config file instead");
|
||||||
|
}
|
||||||
// load and parse the file
|
// load and parse the file
|
||||||
std::fs::read_to_string(config_path).with_context(|| {
|
let contents = std::fs::read_to_string(config_path).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
"Could not read configuration file '{}'",
|
"Could not read configuration file '{}'",
|
||||||
config_path.display()
|
config_path.display()
|
||||||
)
|
)
|
||||||
})?
|
})?;
|
||||||
|
toml_edit::de::from_str(&contents)?
|
||||||
} else {
|
} else {
|
||||||
// Built-in default config
|
// User (likely interactive) did not provide a description of the environment, give them the default
|
||||||
default_conf(*num_pageservers)
|
NeonLocalInitConf {
|
||||||
|
control_plane_api: Some(Some(DEFAULT_PAGESERVER_CONTROL_PLANE_API.parse().unwrap())),
|
||||||
|
broker: NeonBroker {
|
||||||
|
listen_addr: DEFAULT_BROKER_ADDR.parse().unwrap(),
|
||||||
|
},
|
||||||
|
safekeepers: vec![SafekeeperConf {
|
||||||
|
id: DEFAULT_SAFEKEEPER_ID,
|
||||||
|
pg_port: DEFAULT_SAFEKEEPER_PG_PORT,
|
||||||
|
http_port: DEFAULT_SAFEKEEPER_HTTP_PORT,
|
||||||
|
..Default::default()
|
||||||
|
}],
|
||||||
|
pageservers: (0..num_pageservers.copied().unwrap_or(1))
|
||||||
|
.map(|i| {
|
||||||
|
let pageserver_id = NodeId(DEFAULT_PAGESERVER_ID.0 + i as u64);
|
||||||
|
let pg_port = DEFAULT_PAGESERVER_PG_PORT + i;
|
||||||
|
let http_port = DEFAULT_PAGESERVER_HTTP_PORT + i;
|
||||||
|
NeonLocalInitPageserverConf {
|
||||||
|
id: pageserver_id,
|
||||||
|
listen_pg_addr: format!("127.0.0.1:{pg_port}"),
|
||||||
|
listen_http_addr: format!("127.0.0.1:{http_port}"),
|
||||||
|
pg_auth_type: AuthType::Trust,
|
||||||
|
http_auth_type: AuthType::Trust,
|
||||||
|
other: Default::default(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
pg_distrib_dir: None,
|
||||||
|
neon_distrib_dir: None,
|
||||||
|
default_tenant_id: TenantId::from_array(std::array::from_fn(|_| 0)),
|
||||||
|
storage_controller: None,
|
||||||
|
control_plane_compute_hook_api: None,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let pg_version = init_match
|
LocalEnv::init(init_conf, force)
|
||||||
.get_one::<u32>("pg-version")
|
.context("materialize initial neon_local environment on disk")?;
|
||||||
.copied()
|
Ok(LocalEnv::load_config(&local_env::base_path())
|
||||||
.context("Failed to parse postgres version from the argument string")?;
|
.expect("freshly written config should be loadable"))
|
||||||
|
|
||||||
let mut env =
|
|
||||||
LocalEnv::parse_config(&toml_file).context("Failed to create neon configuration")?;
|
|
||||||
let force = init_match.get_one("force").expect("we set a default value");
|
|
||||||
env.init(pg_version, force)
|
|
||||||
.context("Failed to initialize neon repository")?;
|
|
||||||
|
|
||||||
// Create remote storage location for default LocalFs remote storage
|
|
||||||
std::fs::create_dir_all(env.base_data_dir.join(PAGESERVER_REMOTE_STORAGE_DIR))?;
|
|
||||||
|
|
||||||
// Initialize pageserver, create initial tenant and timeline.
|
|
||||||
for ps_conf in &env.pageservers {
|
|
||||||
PageServerNode::from_env(&env, ps_conf)
|
|
||||||
.initialize(&pageserver_config_overrides(init_match))
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
eprintln!("pageserver init failed: {e:?}");
|
|
||||||
exit(1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(env)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The default pageserver is the one where CLI tenant/timeline operations are sent by default.
|
/// The default pageserver is the one where CLI tenant/timeline operations are sent by default.
|
||||||
@@ -399,15 +382,6 @@ fn get_default_pageserver(env: &local_env::LocalEnv) -> PageServerNode {
|
|||||||
PageServerNode::from_env(env, ps_conf)
|
PageServerNode::from_env(env, ps_conf)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageserver_config_overrides(init_match: &ArgMatches) -> Vec<&str> {
|
|
||||||
init_match
|
|
||||||
.get_many::<String>("pageserver-config-override")
|
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
|
||||||
.map(String::as_str)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_tenant(
|
async fn handle_tenant(
|
||||||
tenant_match: &ArgMatches,
|
tenant_match: &ArgMatches,
|
||||||
env: &mut local_env::LocalEnv,
|
env: &mut local_env::LocalEnv,
|
||||||
@@ -419,6 +393,54 @@ async fn handle_tenant(
|
|||||||
println!("{} {:?}", t.id, t.state);
|
println!("{} {:?}", t.id, t.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(("import", import_match)) => {
|
||||||
|
let tenant_id = parse_tenant_id(import_match)?.unwrap_or_else(TenantId::generate);
|
||||||
|
|
||||||
|
let storage_controller = StorageController::from_env(env);
|
||||||
|
let create_response = storage_controller.tenant_import(tenant_id).await?;
|
||||||
|
|
||||||
|
let shard_zero = create_response
|
||||||
|
.shards
|
||||||
|
.first()
|
||||||
|
.expect("Import response omitted shards");
|
||||||
|
|
||||||
|
let attached_pageserver_id = shard_zero.node_id;
|
||||||
|
let pageserver =
|
||||||
|
PageServerNode::from_env(env, env.get_pageserver_conf(attached_pageserver_id)?);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Imported tenant {tenant_id}, attached to pageserver {attached_pageserver_id}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let timelines = pageserver
|
||||||
|
.http_client
|
||||||
|
.list_timelines(shard_zero.shard_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Pick a 'main' timeline that has no ancestors, the rest will get arbitrary names
|
||||||
|
let main_timeline = timelines
|
||||||
|
.iter()
|
||||||
|
.find(|t| t.ancestor_timeline_id.is_none())
|
||||||
|
.expect("No timelines found")
|
||||||
|
.timeline_id;
|
||||||
|
|
||||||
|
let mut branch_i = 0;
|
||||||
|
for timeline in timelines.iter() {
|
||||||
|
let branch_name = if timeline.timeline_id == main_timeline {
|
||||||
|
"main".to_string()
|
||||||
|
} else {
|
||||||
|
branch_i += 1;
|
||||||
|
format!("branch_{branch_i}")
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Importing timeline {tenant_id}/{} as branch {branch_name}",
|
||||||
|
timeline.timeline_id
|
||||||
|
);
|
||||||
|
|
||||||
|
env.register_branch_mapping(branch_name, tenant_id, timeline.timeline_id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some(("create", create_match)) => {
|
Some(("create", create_match)) => {
|
||||||
let tenant_conf: HashMap<_, _> = create_match
|
let tenant_conf: HashMap<_, _> = create_match
|
||||||
.get_many::<String>("config")
|
.get_many::<String>("config")
|
||||||
@@ -578,13 +600,9 @@ async fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::Local
|
|||||||
Some(("import", import_match)) => {
|
Some(("import", import_match)) => {
|
||||||
let tenant_id = get_tenant_id(import_match, env)?;
|
let tenant_id = get_tenant_id(import_match, env)?;
|
||||||
let timeline_id = parse_timeline_id(import_match)?.expect("No timeline id provided");
|
let timeline_id = parse_timeline_id(import_match)?.expect("No timeline id provided");
|
||||||
let name = import_match
|
let branch_name = import_match
|
||||||
.get_one::<String>("node-name")
|
.get_one::<String>("branch-name")
|
||||||
.ok_or_else(|| anyhow!("No node name provided"))?;
|
.ok_or_else(|| anyhow!("No branch name provided"))?;
|
||||||
let update_catalog = import_match
|
|
||||||
.get_one::<bool>("update-catalog")
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Parse base inputs
|
// Parse base inputs
|
||||||
let base_tarfile = import_match
|
let base_tarfile = import_match
|
||||||
@@ -611,24 +629,11 @@ async fn handle_timeline(timeline_match: &ArgMatches, env: &mut local_env::Local
|
|||||||
.copied()
|
.copied()
|
||||||
.context("Failed to parse postgres version from the argument string")?;
|
.context("Failed to parse postgres version from the argument string")?;
|
||||||
|
|
||||||
let mut cplane = ComputeControlPlane::load(env.clone())?;
|
|
||||||
println!("Importing timeline into pageserver ...");
|
println!("Importing timeline into pageserver ...");
|
||||||
pageserver
|
pageserver
|
||||||
.timeline_import(tenant_id, timeline_id, base, pg_wal, pg_version)
|
.timeline_import(tenant_id, timeline_id, base, pg_wal, pg_version)
|
||||||
.await?;
|
.await?;
|
||||||
env.register_branch_mapping(name.to_string(), tenant_id, timeline_id)?;
|
env.register_branch_mapping(branch_name.to_string(), tenant_id, timeline_id)?;
|
||||||
|
|
||||||
println!("Creating endpoint for imported timeline ...");
|
|
||||||
cplane.new_endpoint(
|
|
||||||
name,
|
|
||||||
tenant_id,
|
|
||||||
timeline_id,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
pg_version,
|
|
||||||
ComputeMode::Primary,
|
|
||||||
!update_catalog,
|
|
||||||
)?;
|
|
||||||
println!("Done");
|
println!("Done");
|
||||||
}
|
}
|
||||||
Some(("branch", branch_match)) => {
|
Some(("branch", branch_match)) => {
|
||||||
@@ -791,6 +796,8 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
.copied()
|
.copied()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let allow_multiple = sub_args.get_flag("allow-multiple");
|
||||||
|
|
||||||
let mode = match (lsn, hot_standby) {
|
let mode = match (lsn, hot_standby) {
|
||||||
(Some(lsn), false) => ComputeMode::Static(lsn),
|
(Some(lsn), false) => ComputeMode::Static(lsn),
|
||||||
(None, true) => ComputeMode::Replica,
|
(None, true) => ComputeMode::Replica,
|
||||||
@@ -808,7 +815,9 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
cplane.check_conflicting_endpoints(mode, tenant_id, timeline_id)?;
|
if !allow_multiple {
|
||||||
|
cplane.check_conflicting_endpoints(mode, tenant_id, timeline_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
cplane.new_endpoint(
|
cplane.new_endpoint(
|
||||||
&endpoint_id,
|
&endpoint_id,
|
||||||
@@ -837,20 +846,15 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
|
|
||||||
let remote_ext_config = sub_args.get_one::<String>("remote-ext-config");
|
let remote_ext_config = sub_args.get_one::<String>("remote-ext-config");
|
||||||
|
|
||||||
// If --safekeepers argument is given, use only the listed safekeeper nodes.
|
let allow_multiple = sub_args.get_flag("allow-multiple");
|
||||||
let safekeepers =
|
|
||||||
if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
|
// If --safekeepers argument is given, use only the listed
|
||||||
let mut safekeepers: Vec<NodeId> = Vec::new();
|
// safekeeper nodes; otherwise all from the env.
|
||||||
for sk_id in safekeepers_str.split(',').map(str::trim) {
|
let safekeepers = if let Some(safekeepers) = parse_safekeepers(sub_args)? {
|
||||||
let sk_id = NodeId(u64::from_str(sk_id).map_err(|_| {
|
safekeepers
|
||||||
anyhow!("invalid node ID \"{sk_id}\" in --safekeepers list")
|
} else {
|
||||||
})?);
|
env.safekeepers.iter().map(|sk| sk.id).collect()
|
||||||
safekeepers.push(sk_id);
|
};
|
||||||
}
|
|
||||||
safekeepers
|
|
||||||
} else {
|
|
||||||
env.safekeepers.iter().map(|sk| sk.id).collect()
|
|
||||||
};
|
|
||||||
|
|
||||||
let endpoint = cplane
|
let endpoint = cplane
|
||||||
.endpoints
|
.endpoints
|
||||||
@@ -862,11 +866,13 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
.cloned()
|
.cloned()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
cplane.check_conflicting_endpoints(
|
if !allow_multiple {
|
||||||
endpoint.mode,
|
cplane.check_conflicting_endpoints(
|
||||||
endpoint.tenant_id,
|
endpoint.mode,
|
||||||
endpoint.timeline_id,
|
endpoint.tenant_id,
|
||||||
)?;
|
endpoint.timeline_id,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
let (pageservers, stripe_size) = if let Some(pageserver_id) = pageserver_id {
|
let (pageservers, stripe_size) = if let Some(pageserver_id) = pageserver_id {
|
||||||
let conf = env.get_pageserver_conf(pageserver_id).unwrap();
|
let conf = env.get_pageserver_conf(pageserver_id).unwrap();
|
||||||
@@ -952,7 +958,10 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
};
|
};
|
||||||
endpoint.reconfigure(pageservers, None).await?;
|
// If --safekeepers argument is given, use only the listed
|
||||||
|
// safekeeper nodes; otherwise all from the env.
|
||||||
|
let safekeepers = parse_safekeepers(sub_args)?;
|
||||||
|
endpoint.reconfigure(pageservers, None, safekeepers).await?;
|
||||||
}
|
}
|
||||||
"stop" => {
|
"stop" => {
|
||||||
let endpoint_id = sub_args
|
let endpoint_id = sub_args
|
||||||
@@ -974,6 +983,23 @@ async fn handle_endpoint(ep_match: &ArgMatches, env: &local_env::LocalEnv) -> Re
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse --safekeepers as list of safekeeper ids.
|
||||||
|
fn parse_safekeepers(sub_args: &ArgMatches) -> Result<Option<Vec<NodeId>>> {
|
||||||
|
if let Some(safekeepers_str) = sub_args.get_one::<String>("safekeepers") {
|
||||||
|
let mut safekeepers: Vec<NodeId> = Vec::new();
|
||||||
|
for sk_id in safekeepers_str.split(',').map(str::trim) {
|
||||||
|
let sk_id = NodeId(
|
||||||
|
u64::from_str(sk_id)
|
||||||
|
.map_err(|_| anyhow!("invalid node ID \"{sk_id}\" in --safekeepers list"))?,
|
||||||
|
);
|
||||||
|
safekeepers.push(sk_id);
|
||||||
|
}
|
||||||
|
Ok(Some(safekeepers))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn handle_mappings(sub_match: &ArgMatches, env: &mut local_env::LocalEnv) -> Result<()> {
|
fn handle_mappings(sub_match: &ArgMatches, env: &mut local_env::LocalEnv) -> Result<()> {
|
||||||
let (sub_name, sub_args) = match sub_match.subcommand() {
|
let (sub_name, sub_args) = match sub_match.subcommand() {
|
||||||
Some(ep_subcommand_data) => ep_subcommand_data,
|
Some(ep_subcommand_data) => ep_subcommand_data,
|
||||||
@@ -1019,11 +1045,18 @@ fn get_pageserver(env: &local_env::LocalEnv, args: &ArgMatches) -> Result<PageSe
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_start_timeout(args: &ArgMatches) -> &Duration {
|
||||||
|
let humantime_duration = args
|
||||||
|
.get_one::<humantime::Duration>("start-timeout")
|
||||||
|
.expect("invalid value for start-timeout");
|
||||||
|
humantime_duration.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
|
async fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> Result<()> {
|
||||||
match sub_match.subcommand() {
|
match sub_match.subcommand() {
|
||||||
Some(("start", subcommand_args)) => {
|
Some(("start", subcommand_args)) => {
|
||||||
if let Err(e) = get_pageserver(env, subcommand_args)?
|
if let Err(e) = get_pageserver(env, subcommand_args)?
|
||||||
.start(&pageserver_config_overrides(subcommand_args))
|
.start(get_start_timeout(subcommand_args))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
eprintln!("pageserver start failed: {e}");
|
eprintln!("pageserver start failed: {e}");
|
||||||
@@ -1051,30 +1084,12 @@ async fn handle_pageserver(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = pageserver
|
if let Err(e) = pageserver.start(get_start_timeout(sub_match)).await {
|
||||||
.start(&pageserver_config_overrides(subcommand_args))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
eprintln!("pageserver start failed: {e}");
|
eprintln!("pageserver start failed: {e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(("set-state", subcommand_args)) => {
|
|
||||||
let pageserver = get_pageserver(env, subcommand_args)?;
|
|
||||||
let scheduling = subcommand_args.get_one("scheduling");
|
|
||||||
let availability = subcommand_args.get_one("availability");
|
|
||||||
|
|
||||||
let storage_controller = StorageController::from_env(env);
|
|
||||||
storage_controller
|
|
||||||
.node_configure(NodeConfigureRequest {
|
|
||||||
node_id: pageserver.conf.id,
|
|
||||||
scheduling: scheduling.cloned(),
|
|
||||||
availability: availability.cloned(),
|
|
||||||
})
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(("status", subcommand_args)) => {
|
Some(("status", subcommand_args)) => {
|
||||||
match get_pageserver(env, subcommand_args)?.check_status().await {
|
match get_pageserver(env, subcommand_args)?.check_status().await {
|
||||||
Ok(_) => println!("Page server is up and running"),
|
Ok(_) => println!("Page server is up and running"),
|
||||||
@@ -1097,8 +1112,8 @@ async fn handle_storage_controller(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let svc = StorageController::from_env(env);
|
let svc = StorageController::from_env(env);
|
||||||
match sub_match.subcommand() {
|
match sub_match.subcommand() {
|
||||||
Some(("start", _start_match)) => {
|
Some(("start", start_match)) => {
|
||||||
if let Err(e) = svc.start().await {
|
if let Err(e) = svc.start(get_start_timeout(start_match)).await {
|
||||||
eprintln!("start failed: {e}");
|
eprintln!("start failed: {e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -1157,7 +1172,10 @@ async fn handle_safekeeper(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
"start" => {
|
"start" => {
|
||||||
let extra_opts = safekeeper_extra_opts(sub_args);
|
let extra_opts = safekeeper_extra_opts(sub_args);
|
||||||
|
|
||||||
if let Err(e) = safekeeper.start(extra_opts).await {
|
if let Err(e) = safekeeper
|
||||||
|
.start(extra_opts, get_start_timeout(sub_args))
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("safekeeper start failed: {}", e);
|
eprintln!("safekeeper start failed: {}", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -1183,7 +1201,10 @@ async fn handle_safekeeper(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
let extra_opts = safekeeper_extra_opts(sub_args);
|
let extra_opts = safekeeper_extra_opts(sub_args);
|
||||||
if let Err(e) = safekeeper.start(extra_opts).await {
|
if let Err(e) = safekeeper
|
||||||
|
.start(extra_opts, get_start_timeout(sub_args))
|
||||||
|
.await
|
||||||
|
{
|
||||||
eprintln!("safekeeper start failed: {}", e);
|
eprintln!("safekeeper start failed: {}", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
@@ -1196,15 +1217,18 @@ async fn handle_safekeeper(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_start_all(sub_match: &ArgMatches, env: &local_env::LocalEnv) -> anyhow::Result<()> {
|
async fn handle_start_all(
|
||||||
|
env: &local_env::LocalEnv,
|
||||||
|
retry_timeout: &Duration,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
// Endpoints are not started automatically
|
// Endpoints are not started automatically
|
||||||
|
|
||||||
broker::start_broker_process(env).await?;
|
broker::start_broker_process(env, retry_timeout).await?;
|
||||||
|
|
||||||
// Only start the storage controller if the pageserver is configured to need it
|
// Only start the storage controller if the pageserver is configured to need it
|
||||||
if env.control_plane_api.is_some() {
|
if env.control_plane_api.is_some() {
|
||||||
let storage_controller = StorageController::from_env(env);
|
let storage_controller = StorageController::from_env(env);
|
||||||
if let Err(e) = storage_controller.start().await {
|
if let Err(e) = storage_controller.start(retry_timeout).await {
|
||||||
eprintln!("storage_controller start failed: {:#}", e);
|
eprintln!("storage_controller start failed: {:#}", e);
|
||||||
try_stop_all(env, true).await;
|
try_stop_all(env, true).await;
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -1213,10 +1237,7 @@ async fn handle_start_all(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
|
|
||||||
for ps_conf in &env.pageservers {
|
for ps_conf in &env.pageservers {
|
||||||
let pageserver = PageServerNode::from_env(env, ps_conf);
|
let pageserver = PageServerNode::from_env(env, ps_conf);
|
||||||
if let Err(e) = pageserver
|
if let Err(e) = pageserver.start(retry_timeout).await {
|
||||||
.start(&pageserver_config_overrides(sub_match))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
eprintln!("pageserver {} start failed: {:#}", ps_conf.id, e);
|
eprintln!("pageserver {} start failed: {:#}", ps_conf.id, e);
|
||||||
try_stop_all(env, true).await;
|
try_stop_all(env, true).await;
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -1225,7 +1246,7 @@ async fn handle_start_all(sub_match: &ArgMatches, env: &local_env::LocalEnv) ->
|
|||||||
|
|
||||||
for node in env.safekeepers.iter() {
|
for node in env.safekeepers.iter() {
|
||||||
let safekeeper = SafekeeperNode::from_env(env, node);
|
let safekeeper = SafekeeperNode::from_env(env, node);
|
||||||
if let Err(e) = safekeeper.start(vec![]).await {
|
if let Err(e) = safekeeper.start(vec![], retry_timeout).await {
|
||||||
eprintln!("safekeeper {} start failed: {:#}", safekeeper.id, e);
|
eprintln!("safekeeper {} start failed: {:#}", safekeeper.id, e);
|
||||||
try_stop_all(env, false).await;
|
try_stop_all(env, false).await;
|
||||||
exit(1);
|
exit(1);
|
||||||
@@ -1248,7 +1269,7 @@ async fn try_stop_all(env: &local_env::LocalEnv, immediate: bool) {
|
|||||||
match ComputeControlPlane::load(env.clone()) {
|
match ComputeControlPlane::load(env.clone()) {
|
||||||
Ok(cplane) => {
|
Ok(cplane) => {
|
||||||
for (_k, node) in cplane.endpoints {
|
for (_k, node) in cplane.endpoints {
|
||||||
if let Err(e) = node.stop(if immediate { "immediate" } else { "fast " }, false) {
|
if let Err(e) = node.stop(if immediate { "immediate" } else { "fast" }, false) {
|
||||||
eprintln!("postgres stop failed: {e:#}");
|
eprintln!("postgres stop failed: {e:#}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1285,6 +1306,15 @@ async fn try_stop_all(env: &local_env::LocalEnv, immediate: bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn cli() -> Command {
|
fn cli() -> Command {
|
||||||
|
let timeout_arg = Arg::new("start-timeout")
|
||||||
|
.long("start-timeout")
|
||||||
|
.short('t')
|
||||||
|
.global(true)
|
||||||
|
.help("timeout until we fail the command, e.g. 30s")
|
||||||
|
.value_parser(value_parser!(humantime::Duration))
|
||||||
|
.default_value("10s")
|
||||||
|
.required(false);
|
||||||
|
|
||||||
let branch_name_arg = Arg::new("branch-name")
|
let branch_name_arg = Arg::new("branch-name")
|
||||||
.long("branch-name")
|
.long("branch-name")
|
||||||
.help("Name of the branch to be created or used as an alias for other services")
|
.help("Name of the branch to be created or used as an alias for other services")
|
||||||
@@ -1357,13 +1387,6 @@ fn cli() -> Command {
|
|||||||
.required(false)
|
.required(false)
|
||||||
.value_name("stop-mode");
|
.value_name("stop-mode");
|
||||||
|
|
||||||
let pageserver_config_args = Arg::new("pageserver-config-override")
|
|
||||||
.long("pageserver-config-override")
|
|
||||||
.num_args(1)
|
|
||||||
.action(ArgAction::Append)
|
|
||||||
.help("Additional pageserver's configuration options or overrides, refer to pageserver's 'config-override' CLI parameter docs for more")
|
|
||||||
.required(false);
|
|
||||||
|
|
||||||
let remote_ext_config_args = Arg::new("remote-ext-config")
|
let remote_ext_config_args = Arg::new("remote-ext-config")
|
||||||
.long("remote-ext-config")
|
.long("remote-ext-config")
|
||||||
.num_args(1)
|
.num_args(1)
|
||||||
@@ -1397,9 +1420,7 @@ fn cli() -> Command {
|
|||||||
let num_pageservers_arg = Arg::new("num-pageservers")
|
let num_pageservers_arg = Arg::new("num-pageservers")
|
||||||
.value_parser(value_parser!(u16))
|
.value_parser(value_parser!(u16))
|
||||||
.long("num-pageservers")
|
.long("num-pageservers")
|
||||||
.help("How many pageservers to create (default 1)")
|
.help("How many pageservers to create (default 1)");
|
||||||
.required(false)
|
|
||||||
.default_value("1");
|
|
||||||
|
|
||||||
let update_catalog = Arg::new("update-catalog")
|
let update_catalog = Arg::new("update-catalog")
|
||||||
.value_parser(value_parser!(bool))
|
.value_parser(value_parser!(bool))
|
||||||
@@ -1413,20 +1434,25 @@ fn cli() -> Command {
|
|||||||
.help("If set, will create test user `user` and `neondb` database. Requires `update-catalog = true`")
|
.help("If set, will create test user `user` and `neondb` database. Requires `update-catalog = true`")
|
||||||
.required(false);
|
.required(false);
|
||||||
|
|
||||||
|
let allow_multiple = Arg::new("allow-multiple")
|
||||||
|
.help("Allow multiple primary endpoints running on the same branch. Shouldn't be used normally, but useful for tests.")
|
||||||
|
.long("allow-multiple")
|
||||||
|
.action(ArgAction::SetTrue)
|
||||||
|
.required(false);
|
||||||
|
|
||||||
Command::new("Neon CLI")
|
Command::new("Neon CLI")
|
||||||
.arg_required_else_help(true)
|
.arg_required_else_help(true)
|
||||||
.version(GIT_VERSION)
|
.version(GIT_VERSION)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("init")
|
Command::new("init")
|
||||||
.about("Initialize a new Neon repository, preparing configs for services to start with")
|
.about("Initialize a new Neon repository, preparing configs for services to start with")
|
||||||
.arg(pageserver_config_args.clone())
|
|
||||||
.arg(num_pageservers_arg.clone())
|
.arg(num_pageservers_arg.clone())
|
||||||
.arg(
|
.arg(
|
||||||
Arg::new("config")
|
Arg::new("config")
|
||||||
.long("config")
|
.long("config")
|
||||||
.required(false)
|
.required(false)
|
||||||
.value_parser(value_parser!(PathBuf))
|
.value_parser(value_parser!(PathBuf))
|
||||||
.value_name("config"),
|
.value_name("config")
|
||||||
)
|
)
|
||||||
.arg(pg_version_arg.clone())
|
.arg(pg_version_arg.clone())
|
||||||
.arg(force_arg)
|
.arg(force_arg)
|
||||||
@@ -1434,6 +1460,7 @@ fn cli() -> Command {
|
|||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("timeline")
|
Command::new("timeline")
|
||||||
.about("Manage timelines")
|
.about("Manage timelines")
|
||||||
|
.arg_required_else_help(true)
|
||||||
.subcommand(Command::new("list")
|
.subcommand(Command::new("list")
|
||||||
.about("List all timelines, available to this pageserver")
|
.about("List all timelines, available to this pageserver")
|
||||||
.arg(tenant_id_arg.clone()))
|
.arg(tenant_id_arg.clone()))
|
||||||
@@ -1456,8 +1483,7 @@ fn cli() -> Command {
|
|||||||
.about("Import timeline from basebackup directory")
|
.about("Import timeline from basebackup directory")
|
||||||
.arg(tenant_id_arg.clone())
|
.arg(tenant_id_arg.clone())
|
||||||
.arg(timeline_id_arg.clone())
|
.arg(timeline_id_arg.clone())
|
||||||
.arg(Arg::new("node-name").long("node-name")
|
.arg(branch_name_arg.clone())
|
||||||
.help("Name to assign to the imported timeline"))
|
|
||||||
.arg(Arg::new("base-tarfile")
|
.arg(Arg::new("base-tarfile")
|
||||||
.long("base-tarfile")
|
.long("base-tarfile")
|
||||||
.value_parser(value_parser!(PathBuf))
|
.value_parser(value_parser!(PathBuf))
|
||||||
@@ -1473,7 +1499,6 @@ fn cli() -> Command {
|
|||||||
.arg(Arg::new("end-lsn").long("end-lsn")
|
.arg(Arg::new("end-lsn").long("end-lsn")
|
||||||
.help("Lsn the basebackup ends at"))
|
.help("Lsn the basebackup ends at"))
|
||||||
.arg(pg_version_arg.clone())
|
.arg(pg_version_arg.clone())
|
||||||
.arg(update_catalog.clone())
|
|
||||||
)
|
)
|
||||||
).subcommand(
|
).subcommand(
|
||||||
Command::new("tenant")
|
Command::new("tenant")
|
||||||
@@ -1496,6 +1521,8 @@ fn cli() -> Command {
|
|||||||
.subcommand(Command::new("config")
|
.subcommand(Command::new("config")
|
||||||
.arg(tenant_id_arg.clone())
|
.arg(tenant_id_arg.clone())
|
||||||
.arg(Arg::new("config").short('c').num_args(1).action(ArgAction::Append).required(false)))
|
.arg(Arg::new("config").short('c').num_args(1).action(ArgAction::Append).required(false)))
|
||||||
|
.subcommand(Command::new("import").arg(tenant_id_arg.clone().required(true))
|
||||||
|
.about("Import a tenant that is present in remote storage, and create branches for its timelines"))
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("pageserver")
|
Command::new("pageserver")
|
||||||
@@ -1505,7 +1532,7 @@ fn cli() -> Command {
|
|||||||
.subcommand(Command::new("status"))
|
.subcommand(Command::new("status"))
|
||||||
.subcommand(Command::new("start")
|
.subcommand(Command::new("start")
|
||||||
.about("Start local pageserver")
|
.about("Start local pageserver")
|
||||||
.arg(pageserver_config_args.clone())
|
.arg(timeout_arg.clone())
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("stop")
|
.subcommand(Command::new("stop")
|
||||||
.about("Stop local pageserver")
|
.about("Stop local pageserver")
|
||||||
@@ -1513,21 +1540,16 @@ fn cli() -> Command {
|
|||||||
)
|
)
|
||||||
.subcommand(Command::new("restart")
|
.subcommand(Command::new("restart")
|
||||||
.about("Restart local pageserver")
|
.about("Restart local pageserver")
|
||||||
.arg(pageserver_config_args.clone())
|
.arg(timeout_arg.clone())
|
||||||
)
|
|
||||||
.subcommand(Command::new("set-state")
|
|
||||||
.arg(Arg::new("availability").value_parser(value_parser!(NodeAvailability)).long("availability").action(ArgAction::Set).help("Availability state: offline,active"))
|
|
||||||
.arg(Arg::new("scheduling").value_parser(value_parser!(NodeSchedulingPolicy)).long("scheduling").action(ArgAction::Set).help("Scheduling state: draining,pause,filling,active"))
|
|
||||||
.about("Set scheduling or availability state of pageserver node")
|
|
||||||
.arg(pageserver_config_args.clone())
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("storage_controller")
|
Command::new("storage_controller")
|
||||||
.arg_required_else_help(true)
|
.arg_required_else_help(true)
|
||||||
.about("Manage storage_controller")
|
.about("Manage storage_controller")
|
||||||
.subcommand(Command::new("start").about("Start local pageserver").arg(pageserver_config_args.clone()))
|
.subcommand(Command::new("start").about("Start storage controller")
|
||||||
.subcommand(Command::new("stop").about("Stop local pageserver")
|
.arg(timeout_arg.clone()))
|
||||||
|
.subcommand(Command::new("stop").about("Stop storage controller")
|
||||||
.arg(stop_mode_arg.clone()))
|
.arg(stop_mode_arg.clone()))
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
@@ -1538,6 +1560,7 @@ fn cli() -> Command {
|
|||||||
.about("Start local safekeeper")
|
.about("Start local safekeeper")
|
||||||
.arg(safekeeper_id_arg.clone())
|
.arg(safekeeper_id_arg.clone())
|
||||||
.arg(safekeeper_extra_opt_arg.clone())
|
.arg(safekeeper_extra_opt_arg.clone())
|
||||||
|
.arg(timeout_arg.clone())
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("stop")
|
.subcommand(Command::new("stop")
|
||||||
.about("Stop local safekeeper")
|
.about("Stop local safekeeper")
|
||||||
@@ -1549,6 +1572,7 @@ fn cli() -> Command {
|
|||||||
.arg(safekeeper_id_arg)
|
.arg(safekeeper_id_arg)
|
||||||
.arg(stop_mode_arg.clone())
|
.arg(stop_mode_arg.clone())
|
||||||
.arg(safekeeper_extra_opt_arg)
|
.arg(safekeeper_extra_opt_arg)
|
||||||
|
.arg(timeout_arg.clone())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
@@ -1573,18 +1597,22 @@ fn cli() -> Command {
|
|||||||
.arg(pg_version_arg.clone())
|
.arg(pg_version_arg.clone())
|
||||||
.arg(hot_standby_arg.clone())
|
.arg(hot_standby_arg.clone())
|
||||||
.arg(update_catalog)
|
.arg(update_catalog)
|
||||||
|
.arg(allow_multiple.clone())
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("start")
|
.subcommand(Command::new("start")
|
||||||
.about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
|
.about("Start postgres.\n If the endpoint doesn't exist yet, it is created.")
|
||||||
.arg(endpoint_id_arg.clone())
|
.arg(endpoint_id_arg.clone())
|
||||||
.arg(endpoint_pageserver_id_arg.clone())
|
.arg(endpoint_pageserver_id_arg.clone())
|
||||||
.arg(safekeepers_arg)
|
.arg(safekeepers_arg.clone())
|
||||||
.arg(remote_ext_config_args)
|
.arg(remote_ext_config_args)
|
||||||
.arg(create_test_user)
|
.arg(create_test_user)
|
||||||
|
.arg(allow_multiple.clone())
|
||||||
|
.arg(timeout_arg.clone())
|
||||||
)
|
)
|
||||||
.subcommand(Command::new("reconfigure")
|
.subcommand(Command::new("reconfigure")
|
||||||
.about("Reconfigure the endpoint")
|
.about("Reconfigure the endpoint")
|
||||||
.arg(endpoint_pageserver_id_arg)
|
.arg(endpoint_pageserver_id_arg)
|
||||||
|
.arg(safekeepers_arg)
|
||||||
.arg(endpoint_id_arg.clone())
|
.arg(endpoint_id_arg.clone())
|
||||||
.arg(tenant_id_arg.clone())
|
.arg(tenant_id_arg.clone())
|
||||||
)
|
)
|
||||||
@@ -1632,7 +1660,7 @@ fn cli() -> Command {
|
|||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("start")
|
Command::new("start")
|
||||||
.about("Start page server and safekeepers")
|
.about("Start page server and safekeepers")
|
||||||
.arg(pageserver_config_args)
|
.arg(timeout_arg.clone())
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
Command::new("stop")
|
Command::new("stop")
|
||||||
|
|||||||
@@ -5,13 +5,18 @@
|
|||||||
//! ```text
|
//! ```text
|
||||||
//! .neon/safekeepers/<safekeeper id>
|
//! .neon/safekeepers/<safekeeper id>
|
||||||
//! ```
|
//! ```
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
|
|
||||||
use crate::{background_process, local_env};
|
use crate::{background_process, local_env};
|
||||||
|
|
||||||
pub async fn start_broker_process(env: &local_env::LocalEnv) -> anyhow::Result<()> {
|
pub async fn start_broker_process(
|
||||||
|
env: &local_env::LocalEnv,
|
||||||
|
retry_timeout: &Duration,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let broker = &env.broker;
|
let broker = &env.broker;
|
||||||
let listen_addr = &broker.listen_addr;
|
let listen_addr = &broker.listen_addr;
|
||||||
|
|
||||||
@@ -27,6 +32,7 @@ pub async fn start_broker_process(env: &local_env::LocalEnv) -> anyhow::Result<(
|
|||||||
args,
|
args,
|
||||||
[],
|
[],
|
||||||
background_process::InitialPidFile::Create(storage_broker_pid_file_path(env)),
|
background_process::InitialPidFile::Create(storage_broker_pid_file_path(env)),
|
||||||
|
retry_timeout,
|
||||||
|| async {
|
|| async {
|
||||||
let url = broker.client_url();
|
let url = broker.client_url();
|
||||||
let status_url = url.join("status").with_context(|| {
|
let status_url = url.join("status").with_context(|| {
|
||||||
|
|||||||
@@ -499,6 +499,23 @@ impl Endpoint {
|
|||||||
.join(",")
|
.join(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map safekeepers ids to the actual connection strings.
|
||||||
|
fn build_safekeepers_connstrs(&self, sk_ids: Vec<NodeId>) -> Result<Vec<String>> {
|
||||||
|
let mut safekeeper_connstrings = Vec::new();
|
||||||
|
if self.mode == ComputeMode::Primary {
|
||||||
|
for sk_id in sk_ids {
|
||||||
|
let sk = self
|
||||||
|
.env
|
||||||
|
.safekeepers
|
||||||
|
.iter()
|
||||||
|
.find(|node| node.id == sk_id)
|
||||||
|
.ok_or_else(|| anyhow!("safekeeper {sk_id} does not exist"))?;
|
||||||
|
safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.get_compute_port()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(safekeeper_connstrings)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
&self,
|
&self,
|
||||||
auth_token: &Option<String>,
|
auth_token: &Option<String>,
|
||||||
@@ -523,18 +540,7 @@ impl Endpoint {
|
|||||||
let pageserver_connstring = Self::build_pageserver_connstr(&pageservers);
|
let pageserver_connstring = Self::build_pageserver_connstr(&pageservers);
|
||||||
assert!(!pageserver_connstring.is_empty());
|
assert!(!pageserver_connstring.is_empty());
|
||||||
|
|
||||||
let mut safekeeper_connstrings = Vec::new();
|
let safekeeper_connstrings = self.build_safekeepers_connstrs(safekeepers)?;
|
||||||
if self.mode == ComputeMode::Primary {
|
|
||||||
for sk_id in safekeepers {
|
|
||||||
let sk = self
|
|
||||||
.env
|
|
||||||
.safekeepers
|
|
||||||
.iter()
|
|
||||||
.find(|node| node.id == sk_id)
|
|
||||||
.ok_or_else(|| anyhow!("safekeeper {sk_id} does not exist"))?;
|
|
||||||
safekeeper_connstrings.push(format!("127.0.0.1:{}", sk.get_compute_port()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for file remote_extensions_spec.json
|
// check for file remote_extensions_spec.json
|
||||||
// if it is present, read it and pass to compute_ctl
|
// if it is present, read it and pass to compute_ctl
|
||||||
@@ -554,6 +560,7 @@ impl Endpoint {
|
|||||||
format_version: 1.0,
|
format_version: 1.0,
|
||||||
operation_uuid: None,
|
operation_uuid: None,
|
||||||
features: self.features.clone(),
|
features: self.features.clone(),
|
||||||
|
swap_size_bytes: None,
|
||||||
cluster: Cluster {
|
cluster: Cluster {
|
||||||
cluster_id: None, // project ID: not used
|
cluster_id: None, // project ID: not used
|
||||||
name: None, // project name: not used
|
name: None, // project name: not used
|
||||||
@@ -591,7 +598,6 @@ impl Endpoint {
|
|||||||
remote_extensions,
|
remote_extensions,
|
||||||
pgbouncer_settings: None,
|
pgbouncer_settings: None,
|
||||||
shard_stripe_size: Some(shard_stripe_size),
|
shard_stripe_size: Some(shard_stripe_size),
|
||||||
primary_is_running: None,
|
|
||||||
};
|
};
|
||||||
let spec_path = self.endpoint_path().join("spec.json");
|
let spec_path = self.endpoint_path().join("spec.json");
|
||||||
std::fs::write(spec_path, serde_json::to_string_pretty(&spec)?)?;
|
std::fs::write(spec_path, serde_json::to_string_pretty(&spec)?)?;
|
||||||
@@ -740,6 +746,7 @@ impl Endpoint {
|
|||||||
&self,
|
&self,
|
||||||
mut pageservers: Vec<(Host, u16)>,
|
mut pageservers: Vec<(Host, u16)>,
|
||||||
stripe_size: Option<ShardStripeSize>,
|
stripe_size: Option<ShardStripeSize>,
|
||||||
|
safekeepers: Option<Vec<NodeId>>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut spec: ComputeSpec = {
|
let mut spec: ComputeSpec = {
|
||||||
let spec_path = self.endpoint_path().join("spec.json");
|
let spec_path = self.endpoint_path().join("spec.json");
|
||||||
@@ -774,6 +781,12 @@ impl Endpoint {
|
|||||||
spec.shard_stripe_size = stripe_size.map(|s| s.0 as usize);
|
spec.shard_stripe_size = stripe_size.map(|s| s.0 as usize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If safekeepers are not specified, don't change them.
|
||||||
|
if let Some(safekeepers) = safekeepers {
|
||||||
|
let safekeeper_connstrings = self.build_safekeepers_connstrs(safekeepers)?;
|
||||||
|
spec.safekeeper_connstrings = safekeeper_connstrings;
|
||||||
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.timeout(Duration::from_secs(30))
|
.timeout(Duration::from_secs(30))
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Now it also provides init method which acts like a stub for proper installation
|
//! Now it also provides init method which acts like a stub for proper installation
|
||||||
//! script which will use local paths.
|
//! script which will use local paths.
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Context};
|
use anyhow::{bail, Context};
|
||||||
|
|
||||||
use clap::ValueEnum;
|
use clap::ValueEnum;
|
||||||
use postgres_backend::AuthType;
|
use postgres_backend::AuthType;
|
||||||
@@ -17,11 +17,14 @@ use std::net::Ipv4Addr;
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Command, Stdio};
|
use std::process::{Command, Stdio};
|
||||||
|
use std::time::Duration;
|
||||||
use utils::{
|
use utils::{
|
||||||
auth::{encode_from_key_file, Claims},
|
auth::{encode_from_key_file, Claims},
|
||||||
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
id::{NodeId, TenantId, TenantTimelineId, TimelineId},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::pageserver::PageServerNode;
|
||||||
|
use crate::pageserver::PAGESERVER_REMOTE_STORAGE_DIR;
|
||||||
use crate::safekeeper::SafekeeperNode;
|
use crate::safekeeper::SafekeeperNode;
|
||||||
|
|
||||||
pub const DEFAULT_PG_VERSION: u32 = 15;
|
pub const DEFAULT_PG_VERSION: u32 = 15;
|
||||||
@@ -33,63 +36,107 @@ pub const DEFAULT_PG_VERSION: u32 = 15;
|
|||||||
// to 'neon_local init --config=<path>' option. See control_plane/simple.conf for
|
// to 'neon_local init --config=<path>' option. See control_plane/simple.conf for
|
||||||
// an example.
|
// an example.
|
||||||
//
|
//
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
#[derive(PartialEq, Eq, Clone, Debug)]
|
||||||
pub struct LocalEnv {
|
pub struct LocalEnv {
|
||||||
// Base directory for all the nodes (the pageserver, safekeepers and
|
// Base directory for all the nodes (the pageserver, safekeepers and
|
||||||
// compute endpoints).
|
// compute endpoints).
|
||||||
//
|
//
|
||||||
// This is not stored in the config file. Rather, this is the path where the
|
// This is not stored in the config file. Rather, this is the path where the
|
||||||
// config file itself is. It is read from the NEON_REPO_DIR env variable or
|
// config file itself is. It is read from the NEON_REPO_DIR env variable which
|
||||||
// '.neon' if not given.
|
// must be an absolute path. If the env var is not set, $PWD/.neon is used.
|
||||||
#[serde(skip)]
|
|
||||||
pub base_data_dir: PathBuf,
|
pub base_data_dir: PathBuf,
|
||||||
|
|
||||||
// Path to postgres distribution. It's expected that "bin", "include",
|
// Path to postgres distribution. It's expected that "bin", "include",
|
||||||
// "lib", "share" from postgres distribution are there. If at some point
|
// "lib", "share" from postgres distribution are there. If at some point
|
||||||
// in time we will be able to run against vanilla postgres we may split that
|
// in time we will be able to run against vanilla postgres we may split that
|
||||||
// to four separate paths and match OS-specific installation layout.
|
// to four separate paths and match OS-specific installation layout.
|
||||||
#[serde(default)]
|
|
||||||
pub pg_distrib_dir: PathBuf,
|
pub pg_distrib_dir: PathBuf,
|
||||||
|
|
||||||
// Path to pageserver binary.
|
// Path to pageserver binary.
|
||||||
#[serde(default)]
|
|
||||||
pub neon_distrib_dir: PathBuf,
|
pub neon_distrib_dir: PathBuf,
|
||||||
|
|
||||||
// Default tenant ID to use with the 'neon_local' command line utility, when
|
// Default tenant ID to use with the 'neon_local' command line utility, when
|
||||||
// --tenant_id is not explicitly specified.
|
// --tenant_id is not explicitly specified.
|
||||||
#[serde(default)]
|
|
||||||
pub default_tenant_id: Option<TenantId>,
|
pub default_tenant_id: Option<TenantId>,
|
||||||
|
|
||||||
// used to issue tokens during e.g pg start
|
// used to issue tokens during e.g pg start
|
||||||
#[serde(default)]
|
|
||||||
pub private_key_path: PathBuf,
|
pub private_key_path: PathBuf,
|
||||||
|
|
||||||
pub broker: NeonBroker,
|
pub broker: NeonBroker,
|
||||||
|
|
||||||
|
// Configuration for the storage controller (1 per neon_local environment)
|
||||||
|
pub storage_controller: NeonStorageControllerConf,
|
||||||
|
|
||||||
/// This Vec must always contain at least one pageserver
|
/// This Vec must always contain at least one pageserver
|
||||||
|
/// Populdated by [`Self::load_config`] from the individual `pageserver.toml`s.
|
||||||
|
/// NB: not used anymore except for informing users that they need to change their `.neon/config`.
|
||||||
pub pageservers: Vec<PageServerConf>,
|
pub pageservers: Vec<PageServerConf>,
|
||||||
|
|
||||||
#[serde(default)]
|
|
||||||
pub safekeepers: Vec<SafekeeperConf>,
|
pub safekeepers: Vec<SafekeeperConf>,
|
||||||
|
|
||||||
// Control plane upcall API for pageserver: if None, we will not run storage_controller If set, this will
|
// Control plane upcall API for pageserver: if None, we will not run storage_controller If set, this will
|
||||||
// be propagated into each pageserver's configuration.
|
// be propagated into each pageserver's configuration.
|
||||||
#[serde(default)]
|
|
||||||
pub control_plane_api: Option<Url>,
|
pub control_plane_api: Option<Url>,
|
||||||
|
|
||||||
// Control plane upcall API for storage controller. If set, this will be propagated into the
|
// Control plane upcall API for storage controller. If set, this will be propagated into the
|
||||||
// storage controller's configuration.
|
// storage controller's configuration.
|
||||||
#[serde(default)]
|
|
||||||
pub control_plane_compute_hook_api: Option<Url>,
|
pub control_plane_compute_hook_api: Option<Url>,
|
||||||
|
|
||||||
/// Keep human-readable aliases in memory (and persist them to config), to hide ZId hex strings from the user.
|
/// Keep human-readable aliases in memory (and persist them to config), to hide ZId hex strings from the user.
|
||||||
#[serde(default)]
|
|
||||||
// A `HashMap<String, HashMap<TenantId, TimelineId>>` would be more appropriate here,
|
// A `HashMap<String, HashMap<TenantId, TimelineId>>` would be more appropriate here,
|
||||||
// but deserialization into a generic toml object as `toml::Value::try_from` fails with an error.
|
// but deserialization into a generic toml object as `toml::Value::try_from` fails with an error.
|
||||||
// https://toml.io/en/v1.0.0 does not contain a concept of "a table inside another table".
|
// https://toml.io/en/v1.0.0 does not contain a concept of "a table inside another table".
|
||||||
|
pub branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On-disk state stored in `.neon/config`.
|
||||||
|
#[derive(PartialEq, Eq, Clone, Debug, Default, Serialize, Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
pub struct OnDiskConfig {
|
||||||
|
pub pg_distrib_dir: PathBuf,
|
||||||
|
pub neon_distrib_dir: PathBuf,
|
||||||
|
pub default_tenant_id: Option<TenantId>,
|
||||||
|
pub private_key_path: PathBuf,
|
||||||
|
pub broker: NeonBroker,
|
||||||
|
pub storage_controller: NeonStorageControllerConf,
|
||||||
|
#[serde(
|
||||||
|
skip_serializing,
|
||||||
|
deserialize_with = "fail_if_pageservers_field_specified"
|
||||||
|
)]
|
||||||
|
pub pageservers: Vec<PageServerConf>,
|
||||||
|
pub safekeepers: Vec<SafekeeperConf>,
|
||||||
|
pub control_plane_api: Option<Url>,
|
||||||
|
pub control_plane_compute_hook_api: Option<Url>,
|
||||||
branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
|
branch_name_mappings: HashMap<String, Vec<(TenantId, TimelineId)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn fail_if_pageservers_field_specified<'de, D>(_: D) -> Result<Vec<PageServerConf>, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
Err(serde::de::Error::custom(
|
||||||
|
"The 'pageservers' field is no longer used; pageserver.toml is now authoritative; \
|
||||||
|
Please remove the `pageservers` from your .neon/config.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The description of the neon_local env to be initialized by `neon_local init --config`.
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct NeonLocalInitConf {
|
||||||
|
// TODO: do we need this? Seems unused
|
||||||
|
pub pg_distrib_dir: Option<PathBuf>,
|
||||||
|
// TODO: do we need this? Seems unused
|
||||||
|
pub neon_distrib_dir: Option<PathBuf>,
|
||||||
|
pub default_tenant_id: TenantId,
|
||||||
|
pub broker: NeonBroker,
|
||||||
|
pub storage_controller: Option<NeonStorageControllerConf>,
|
||||||
|
pub pageservers: Vec<NeonLocalInitPageserverConf>,
|
||||||
|
pub safekeepers: Vec<SafekeeperConf>,
|
||||||
|
pub control_plane_api: Option<Option<Url>>,
|
||||||
|
pub control_plane_compute_hook_api: Option<Option<Url>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Broker config for cluster internal communication.
|
/// Broker config for cluster internal communication.
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -98,6 +145,33 @@ pub struct NeonBroker {
|
|||||||
pub listen_addr: SocketAddr,
|
pub listen_addr: SocketAddr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Broker config for cluster internal communication.
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub struct NeonStorageControllerConf {
|
||||||
|
/// Heartbeat timeout before marking a node offline
|
||||||
|
#[serde(with = "humantime_serde")]
|
||||||
|
pub max_unavailable: Duration,
|
||||||
|
|
||||||
|
/// Threshold for auto-splitting a tenant into shards
|
||||||
|
pub split_threshold: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NeonStorageControllerConf {
|
||||||
|
// Use a shorter pageserver unavailability interval than the default to speed up tests.
|
||||||
|
const DEFAULT_MAX_UNAVAILABLE_INTERVAL: std::time::Duration =
|
||||||
|
std::time::Duration::from_secs(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NeonStorageControllerConf {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
max_unavailable: Self::DEFAULT_MAX_UNAVAILABLE_INTERVAL,
|
||||||
|
split_threshold: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Dummy Default impl to satisfy Deserialize derive.
|
// Dummy Default impl to satisfy Deserialize derive.
|
||||||
impl Default for NeonBroker {
|
impl Default for NeonBroker {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
@@ -113,22 +187,18 @@ impl NeonBroker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// neon_local needs to know this subset of pageserver configuration.
|
||||||
|
// For legacy reasons, this information is duplicated from `pageserver.toml` into `.neon/config`.
|
||||||
|
// It can get stale if `pageserver.toml` is changed.
|
||||||
|
// TODO(christian): don't store this at all in `.neon/config`, always load it from `pageserver.toml`
|
||||||
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct PageServerConf {
|
pub struct PageServerConf {
|
||||||
// node id
|
|
||||||
pub id: NodeId,
|
pub id: NodeId,
|
||||||
|
|
||||||
// Pageserver connection settings
|
|
||||||
pub listen_pg_addr: String,
|
pub listen_pg_addr: String,
|
||||||
pub listen_http_addr: String,
|
pub listen_http_addr: String,
|
||||||
|
|
||||||
// auth type used for the PG and HTTP ports
|
|
||||||
pub pg_auth_type: AuthType,
|
pub pg_auth_type: AuthType,
|
||||||
pub http_auth_type: AuthType,
|
pub http_auth_type: AuthType,
|
||||||
|
|
||||||
pub(crate) virtual_file_io_engine: Option<String>,
|
|
||||||
pub(crate) get_vectored_impl: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PageServerConf {
|
impl Default for PageServerConf {
|
||||||
@@ -139,8 +209,40 @@ impl Default for PageServerConf {
|
|||||||
listen_http_addr: String::new(),
|
listen_http_addr: String::new(),
|
||||||
pg_auth_type: AuthType::Trust,
|
pg_auth_type: AuthType::Trust,
|
||||||
http_auth_type: AuthType::Trust,
|
http_auth_type: AuthType::Trust,
|
||||||
virtual_file_io_engine: None,
|
}
|
||||||
get_vectored_impl: None,
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The toml that can be passed to `neon_local init --config`.
|
||||||
|
/// This is a subset of the `pageserver.toml` configuration.
|
||||||
|
// TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct NeonLocalInitPageserverConf {
|
||||||
|
pub id: NodeId,
|
||||||
|
pub listen_pg_addr: String,
|
||||||
|
pub listen_http_addr: String,
|
||||||
|
pub pg_auth_type: AuthType,
|
||||||
|
pub http_auth_type: AuthType,
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub other: HashMap<String, toml::Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&NeonLocalInitPageserverConf> for PageServerConf {
|
||||||
|
fn from(conf: &NeonLocalInitPageserverConf) -> Self {
|
||||||
|
let NeonLocalInitPageserverConf {
|
||||||
|
id,
|
||||||
|
listen_pg_addr,
|
||||||
|
listen_http_addr,
|
||||||
|
pg_auth_type,
|
||||||
|
http_auth_type,
|
||||||
|
other: _,
|
||||||
|
} = conf;
|
||||||
|
Self {
|
||||||
|
id: *id,
|
||||||
|
listen_pg_addr: listen_pg_addr.clone(),
|
||||||
|
listen_http_addr: listen_http_addr.clone(),
|
||||||
|
pg_auth_type: *pg_auth_type,
|
||||||
|
http_auth_type: *http_auth_type,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,6 +258,7 @@ pub struct SafekeeperConf {
|
|||||||
pub remote_storage: Option<String>,
|
pub remote_storage: Option<String>,
|
||||||
pub backup_threads: Option<u32>,
|
pub backup_threads: Option<u32>,
|
||||||
pub auth_enabled: bool,
|
pub auth_enabled: bool,
|
||||||
|
pub listen_addr: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SafekeeperConf {
|
impl Default for SafekeeperConf {
|
||||||
@@ -169,6 +272,7 @@ impl Default for SafekeeperConf {
|
|||||||
remote_storage: None,
|
remote_storage: None,
|
||||||
backup_threads: None,
|
backup_threads: None,
|
||||||
auth_enabled: false,
|
auth_enabled: false,
|
||||||
|
listen_addr: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,44 +430,8 @@ impl LocalEnv {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a LocalEnv from a config file.
|
/// Construct `Self` from on-disk state.
|
||||||
///
|
pub fn load_config(repopath: &Path) -> anyhow::Result<Self> {
|
||||||
/// Unlike 'load_config', this function fills in any defaults that are missing
|
|
||||||
/// from the config file.
|
|
||||||
pub fn parse_config(toml: &str) -> anyhow::Result<Self> {
|
|
||||||
let mut env: LocalEnv = toml::from_str(toml)?;
|
|
||||||
|
|
||||||
// Find postgres binaries.
|
|
||||||
// Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
|
|
||||||
// Note that later in the code we assume, that distrib dirs follow the same pattern
|
|
||||||
// for all postgres versions.
|
|
||||||
if env.pg_distrib_dir == Path::new("") {
|
|
||||||
if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
|
|
||||||
env.pg_distrib_dir = postgres_bin.into();
|
|
||||||
} else {
|
|
||||||
let cwd = env::current_dir()?;
|
|
||||||
env.pg_distrib_dir = cwd.join("pg_install")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find neon binaries.
|
|
||||||
if env.neon_distrib_dir == Path::new("") {
|
|
||||||
env.neon_distrib_dir = env::current_exe()?.parent().unwrap().to_owned();
|
|
||||||
}
|
|
||||||
|
|
||||||
if env.pageservers.is_empty() {
|
|
||||||
anyhow::bail!("Configuration must contain at least one pageserver");
|
|
||||||
}
|
|
||||||
|
|
||||||
env.base_data_dir = base_path();
|
|
||||||
|
|
||||||
Ok(env)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Locate and load config
|
|
||||||
pub fn load_config() -> anyhow::Result<Self> {
|
|
||||||
let repopath = base_path();
|
|
||||||
|
|
||||||
if !repopath.exists() {
|
if !repopath.exists() {
|
||||||
bail!(
|
bail!(
|
||||||
"Neon config is not found in {}. You need to run 'neon_local init' first",
|
"Neon config is not found in {}. You need to run 'neon_local init' first",
|
||||||
@@ -374,38 +442,129 @@ impl LocalEnv {
|
|||||||
// TODO: check that it looks like a neon repository
|
// TODO: check that it looks like a neon repository
|
||||||
|
|
||||||
// load and parse file
|
// load and parse file
|
||||||
let config = fs::read_to_string(repopath.join("config"))?;
|
let config_file_contents = fs::read_to_string(repopath.join("config"))?;
|
||||||
let mut env: LocalEnv = toml::from_str(config.as_str())?;
|
let on_disk_config: OnDiskConfig = toml::from_str(config_file_contents.as_str())?;
|
||||||
|
let mut env = {
|
||||||
|
let OnDiskConfig {
|
||||||
|
pg_distrib_dir,
|
||||||
|
neon_distrib_dir,
|
||||||
|
default_tenant_id,
|
||||||
|
private_key_path,
|
||||||
|
broker,
|
||||||
|
storage_controller,
|
||||||
|
pageservers,
|
||||||
|
safekeepers,
|
||||||
|
control_plane_api,
|
||||||
|
control_plane_compute_hook_api,
|
||||||
|
branch_name_mappings,
|
||||||
|
} = on_disk_config;
|
||||||
|
LocalEnv {
|
||||||
|
base_data_dir: repopath.to_owned(),
|
||||||
|
pg_distrib_dir,
|
||||||
|
neon_distrib_dir,
|
||||||
|
default_tenant_id,
|
||||||
|
private_key_path,
|
||||||
|
broker,
|
||||||
|
storage_controller,
|
||||||
|
pageservers,
|
||||||
|
safekeepers,
|
||||||
|
control_plane_api,
|
||||||
|
control_plane_compute_hook_api,
|
||||||
|
branch_name_mappings,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
env.base_data_dir = repopath;
|
// The source of truth for pageserver configuration is the pageserver.toml.
|
||||||
|
assert!(
|
||||||
|
env.pageservers.is_empty(),
|
||||||
|
"we ensure this during deserialization"
|
||||||
|
);
|
||||||
|
env.pageservers = {
|
||||||
|
let iter = std::fs::read_dir(repopath).context("open dir")?;
|
||||||
|
let mut pageservers = Vec::new();
|
||||||
|
for res in iter {
|
||||||
|
let dentry = res?;
|
||||||
|
const PREFIX: &str = "pageserver_";
|
||||||
|
let dentry_name = dentry
|
||||||
|
.file_name()
|
||||||
|
.into_string()
|
||||||
|
.ok()
|
||||||
|
.with_context(|| format!("non-utf8 dentry: {:?}", dentry.path()))
|
||||||
|
.unwrap();
|
||||||
|
if !dentry_name.starts_with(PREFIX) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !dentry.file_type().context("determine file type")?.is_dir() {
|
||||||
|
anyhow::bail!("expected a directory, got {:?}", dentry.path());
|
||||||
|
}
|
||||||
|
let id = dentry_name[PREFIX.len()..]
|
||||||
|
.parse::<NodeId>()
|
||||||
|
.with_context(|| format!("parse id from {:?}", dentry.path()))?;
|
||||||
|
// TODO(christian): use pageserver_api::config::ConfigToml (PR #7656)
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
// (allow unknown fields, unlike PageServerConf)
|
||||||
|
struct PageserverConfigTomlSubset {
|
||||||
|
id: NodeId,
|
||||||
|
listen_pg_addr: String,
|
||||||
|
listen_http_addr: String,
|
||||||
|
pg_auth_type: AuthType,
|
||||||
|
http_auth_type: AuthType,
|
||||||
|
}
|
||||||
|
let config_toml_path = dentry.path().join("pageserver.toml");
|
||||||
|
let config_toml: PageserverConfigTomlSubset = toml_edit::de::from_str(
|
||||||
|
&std::fs::read_to_string(&config_toml_path)
|
||||||
|
.with_context(|| format!("read {:?}", config_toml_path))?,
|
||||||
|
)
|
||||||
|
.context("parse pageserver.toml")?;
|
||||||
|
let PageserverConfigTomlSubset {
|
||||||
|
id: config_toml_id,
|
||||||
|
listen_pg_addr,
|
||||||
|
listen_http_addr,
|
||||||
|
pg_auth_type,
|
||||||
|
http_auth_type,
|
||||||
|
} = config_toml;
|
||||||
|
let conf = PageServerConf {
|
||||||
|
id: {
|
||||||
|
anyhow::ensure!(
|
||||||
|
config_toml_id == id,
|
||||||
|
"id mismatch: config_toml.id={config_toml_id} id={id}",
|
||||||
|
);
|
||||||
|
id
|
||||||
|
},
|
||||||
|
listen_pg_addr,
|
||||||
|
listen_http_addr,
|
||||||
|
pg_auth_type,
|
||||||
|
http_auth_type,
|
||||||
|
};
|
||||||
|
pageservers.push(conf);
|
||||||
|
}
|
||||||
|
pageservers
|
||||||
|
};
|
||||||
|
|
||||||
Ok(env)
|
Ok(env)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn persist_config(&self, base_path: &Path) -> anyhow::Result<()> {
|
pub fn persist_config(&self) -> anyhow::Result<()> {
|
||||||
// Currently, the user first passes a config file with 'neon_local init --config=<path>'
|
Self::persist_config_impl(
|
||||||
// We read that in, in `create_config`, and fill any missing defaults. Then it's saved
|
&self.base_data_dir,
|
||||||
// to .neon/config. TODO: We lose any formatting and comments along the way, which is
|
&OnDiskConfig {
|
||||||
// a bit sad.
|
pg_distrib_dir: self.pg_distrib_dir.clone(),
|
||||||
let mut conf_content = r#"# This file describes a local deployment of the page server
|
neon_distrib_dir: self.neon_distrib_dir.clone(),
|
||||||
# and safekeeeper node. It is read by the 'neon_local' command-line
|
default_tenant_id: self.default_tenant_id,
|
||||||
# utility.
|
private_key_path: self.private_key_path.clone(),
|
||||||
"#
|
broker: self.broker.clone(),
|
||||||
.to_string();
|
storage_controller: self.storage_controller.clone(),
|
||||||
|
pageservers: vec![], // it's skip_serializing anyway
|
||||||
// Convert the LocalEnv to a toml file.
|
safekeepers: self.safekeepers.clone(),
|
||||||
//
|
control_plane_api: self.control_plane_api.clone(),
|
||||||
// This could be as simple as this:
|
control_plane_compute_hook_api: self.control_plane_compute_hook_api.clone(),
|
||||||
//
|
branch_name_mappings: self.branch_name_mappings.clone(),
|
||||||
// conf_content += &toml::to_string_pretty(env)?;
|
},
|
||||||
//
|
)
|
||||||
// But it results in a "values must be emitted before tables". I'm not sure
|
}
|
||||||
// why, AFAICS the table, i.e. 'safekeepers: Vec<SafekeeperConf>' is last.
|
|
||||||
// Maybe rust reorders the fields to squeeze avoid padding or something?
|
|
||||||
// In any case, converting to toml::Value first, and serializing that, works.
|
|
||||||
// See https://github.com/alexcrichton/toml-rs/issues/142
|
|
||||||
conf_content += &toml::to_string_pretty(&toml::Value::try_from(self)?)?;
|
|
||||||
|
|
||||||
|
pub fn persist_config_impl(base_path: &Path, config: &OnDiskConfig) -> anyhow::Result<()> {
|
||||||
|
let conf_content = &toml::to_string_pretty(config)?;
|
||||||
let target_config_path = base_path.join("config");
|
let target_config_path = base_path.join("config");
|
||||||
fs::write(&target_config_path, conf_content).with_context(|| {
|
fs::write(&target_config_path, conf_content).with_context(|| {
|
||||||
format!(
|
format!(
|
||||||
@@ -430,17 +589,13 @@ impl LocalEnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
/// Materialize the [`NeonLocalInitConf`] to disk. Called during [`neon_local init`].
|
||||||
// Initialize a new Neon repository
|
pub fn init(conf: NeonLocalInitConf, force: &InitForceMode) -> anyhow::Result<()> {
|
||||||
//
|
let base_path = base_path();
|
||||||
pub fn init(&mut self, pg_version: u32, force: &InitForceMode) -> anyhow::Result<()> {
|
assert_ne!(base_path, Path::new(""));
|
||||||
// check if config already exists
|
let base_path = &base_path;
|
||||||
let base_path = &self.base_data_dir;
|
|
||||||
ensure!(
|
|
||||||
base_path != Path::new(""),
|
|
||||||
"repository base path is missing"
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// create base_path dir
|
||||||
if base_path.exists() {
|
if base_path.exists() {
|
||||||
match force {
|
match force {
|
||||||
InitForceMode::MustNotExist => {
|
InitForceMode::MustNotExist => {
|
||||||
@@ -472,74 +627,115 @@ impl LocalEnv {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.pg_bin_dir(pg_version)?.join("postgres").exists() {
|
|
||||||
bail!(
|
|
||||||
"Can't find postgres binary at {}",
|
|
||||||
self.pg_bin_dir(pg_version)?.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for binary in ["pageserver", "safekeeper"] {
|
|
||||||
if !self.neon_distrib_dir.join(binary).exists() {
|
|
||||||
bail!(
|
|
||||||
"Can't find binary '{binary}' in neon distrib dir '{}'",
|
|
||||||
self.neon_distrib_dir.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !base_path.exists() {
|
if !base_path.exists() {
|
||||||
fs::create_dir(base_path)?;
|
fs::create_dir(base_path)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let NeonLocalInitConf {
|
||||||
|
pg_distrib_dir,
|
||||||
|
neon_distrib_dir,
|
||||||
|
default_tenant_id,
|
||||||
|
broker,
|
||||||
|
storage_controller,
|
||||||
|
pageservers,
|
||||||
|
safekeepers,
|
||||||
|
control_plane_api,
|
||||||
|
control_plane_compute_hook_api,
|
||||||
|
} = conf;
|
||||||
|
|
||||||
|
// Find postgres binaries.
|
||||||
|
// Follow POSTGRES_DISTRIB_DIR if set, otherwise look in "pg_install".
|
||||||
|
// Note that later in the code we assume, that distrib dirs follow the same pattern
|
||||||
|
// for all postgres versions.
|
||||||
|
let pg_distrib_dir = pg_distrib_dir.unwrap_or_else(|| {
|
||||||
|
if let Some(postgres_bin) = env::var_os("POSTGRES_DISTRIB_DIR") {
|
||||||
|
postgres_bin.into()
|
||||||
|
} else {
|
||||||
|
let cwd = env::current_dir().unwrap();
|
||||||
|
cwd.join("pg_install")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find neon binaries.
|
||||||
|
let neon_distrib_dir = neon_distrib_dir
|
||||||
|
.unwrap_or_else(|| env::current_exe().unwrap().parent().unwrap().to_owned());
|
||||||
|
|
||||||
// Generate keypair for JWT.
|
// Generate keypair for JWT.
|
||||||
//
|
//
|
||||||
// The keypair is only needed if authentication is enabled in any of the
|
// The keypair is only needed if authentication is enabled in any of the
|
||||||
// components. For convenience, we generate the keypair even if authentication
|
// components. For convenience, we generate the keypair even if authentication
|
||||||
// is not enabled, so that you can easily enable it after the initialization
|
// is not enabled, so that you can easily enable it after the initialization
|
||||||
// step. However, if the key generation fails, we treat it as non-fatal if
|
// step.
|
||||||
// authentication was not enabled.
|
generate_auth_keys(
|
||||||
if self.private_key_path == PathBuf::new() {
|
base_path.join("auth_private_key.pem").as_path(),
|
||||||
match generate_auth_keys(
|
base_path.join("auth_public_key.pem").as_path(),
|
||||||
base_path.join("auth_private_key.pem").as_path(),
|
)
|
||||||
base_path.join("auth_public_key.pem").as_path(),
|
.context("generate auth keys")?;
|
||||||
) {
|
let private_key_path = PathBuf::from("auth_private_key.pem");
|
||||||
Ok(()) => {
|
|
||||||
self.private_key_path = PathBuf::from("auth_private_key.pem");
|
// create the runtime type because the remaining initialization code below needs
|
||||||
}
|
// a LocalEnv instance op operation
|
||||||
Err(e) => {
|
// TODO: refactor to avoid this, LocalEnv should only be constructed from on-disk state
|
||||||
if !self.auth_keys_needed() {
|
let env = LocalEnv {
|
||||||
eprintln!("Could not generate keypair for JWT authentication: {e}");
|
base_data_dir: base_path.clone(),
|
||||||
eprintln!("Continuing anyway because authentication was not enabled");
|
pg_distrib_dir,
|
||||||
self.private_key_path = PathBuf::from("auth_private_key.pem");
|
neon_distrib_dir,
|
||||||
} else {
|
default_tenant_id: Some(default_tenant_id),
|
||||||
return Err(e);
|
private_key_path,
|
||||||
}
|
broker,
|
||||||
}
|
storage_controller: storage_controller.unwrap_or_default(),
|
||||||
}
|
pageservers: pageservers.iter().map(Into::into).collect(),
|
||||||
|
safekeepers,
|
||||||
|
control_plane_api: control_plane_api.unwrap_or_default(),
|
||||||
|
control_plane_compute_hook_api: control_plane_compute_hook_api.unwrap_or_default(),
|
||||||
|
branch_name_mappings: Default::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// create endpoints dir
|
||||||
|
fs::create_dir_all(env.endpoints_path())?;
|
||||||
|
|
||||||
|
// create safekeeper dirs
|
||||||
|
for safekeeper in &env.safekeepers {
|
||||||
|
fs::create_dir_all(SafekeeperNode::datadir_path_by_id(&env, safekeeper.id))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::create_dir_all(self.endpoints_path())?;
|
// initialize pageserver state
|
||||||
|
for (i, ps) in pageservers.into_iter().enumerate() {
|
||||||
for safekeeper in &self.safekeepers {
|
let runtime_ps = &env.pageservers[i];
|
||||||
fs::create_dir_all(SafekeeperNode::datadir_path_by_id(self, safekeeper.id))?;
|
assert_eq!(&PageServerConf::from(&ps), runtime_ps);
|
||||||
|
fs::create_dir(env.pageserver_data_dir(ps.id))?;
|
||||||
|
PageServerNode::from_env(&env, runtime_ps)
|
||||||
|
.initialize(ps)
|
||||||
|
.context("pageserver init failed")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.persist_config(base_path)
|
// setup remote remote location for default LocalFs remote storage
|
||||||
}
|
std::fs::create_dir_all(env.base_data_dir.join(PAGESERVER_REMOTE_STORAGE_DIR))?;
|
||||||
|
|
||||||
fn auth_keys_needed(&self) -> bool {
|
env.persist_config()
|
||||||
self.pageservers.iter().any(|ps| {
|
|
||||||
ps.pg_auth_type == AuthType::NeonJWT || ps.http_auth_type == AuthType::NeonJWT
|
|
||||||
}) || self.safekeepers.iter().any(|sk| sk.auth_enabled)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base_path() -> PathBuf {
|
pub fn base_path() -> PathBuf {
|
||||||
match std::env::var_os("NEON_REPO_DIR") {
|
let path = match std::env::var_os("NEON_REPO_DIR") {
|
||||||
Some(val) => PathBuf::from(val),
|
Some(val) => {
|
||||||
None => PathBuf::from(".neon"),
|
let path = PathBuf::from(val);
|
||||||
}
|
if !path.is_absolute() {
|
||||||
|
// repeat the env var in the error because our default is always absolute
|
||||||
|
panic!("NEON_REPO_DIR must be an absolute path, got {path:?}");
|
||||||
|
}
|
||||||
|
path
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let pwd = std::env::current_dir()
|
||||||
|
// technically this can fail but it's quite unlikeley
|
||||||
|
.expect("determine current directory");
|
||||||
|
let pwd_abs = pwd.canonicalize().expect("canonicalize current directory");
|
||||||
|
pwd_abs.join(".neon")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(path.is_absolute());
|
||||||
|
path
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a public/private key pair for JWT authentication
|
/// Generate a public/private key pair for JWT authentication
|
||||||
@@ -578,31 +774,3 @@ fn generate_auth_keys(private_key_path: &Path, public_key_path: &Path) -> anyhow
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn simple_conf_parsing() {
|
|
||||||
let simple_conf_toml = include_str!("../simple.conf");
|
|
||||||
let simple_conf_parse_result = LocalEnv::parse_config(simple_conf_toml);
|
|
||||||
assert!(
|
|
||||||
simple_conf_parse_result.is_ok(),
|
|
||||||
"failed to parse simple config {simple_conf_toml}, reason: {simple_conf_parse_result:?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
let string_to_replace = "listen_addr = '127.0.0.1:50051'";
|
|
||||||
let spoiled_url_str = "listen_addr = '!@$XOXO%^&'";
|
|
||||||
let spoiled_url_toml = simple_conf_toml.replace(string_to_replace, spoiled_url_str);
|
|
||||||
assert!(
|
|
||||||
spoiled_url_toml.contains(spoiled_url_str),
|
|
||||||
"Failed to replace string {string_to_replace} in the toml file {simple_conf_toml}"
|
|
||||||
);
|
|
||||||
let spoiled_url_parse_result = LocalEnv::parse_config(&spoiled_url_toml);
|
|
||||||
assert!(
|
|
||||||
spoiled_url_parse_result.is_err(),
|
|
||||||
"expected toml with invalid Url {spoiled_url_toml} to fail the parsing, but got {spoiled_url_parse_result:?}"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,21 +4,21 @@
|
|||||||
//!
|
//!
|
||||||
//! .neon/
|
//! .neon/
|
||||||
//!
|
//!
|
||||||
use std::borrow::Cow;
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::num::NonZeroU64;
|
use std::num::NonZeroU64;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, Context};
|
use anyhow::{bail, Context};
|
||||||
use camino::Utf8PathBuf;
|
use camino::Utf8PathBuf;
|
||||||
use futures::SinkExt;
|
use futures::SinkExt;
|
||||||
use pageserver_api::models::{
|
use pageserver_api::models::{
|
||||||
self, LocationConfig, ShardParameters, TenantHistorySize, TenantInfo, TimelineInfo,
|
self, AuxFilePolicy, LocationConfig, ShardParameters, TenantHistorySize, TenantInfo,
|
||||||
|
TimelineInfo,
|
||||||
};
|
};
|
||||||
use pageserver_api::shard::TenantShardId;
|
use pageserver_api::shard::TenantShardId;
|
||||||
use pageserver_client::mgmt_api;
|
use pageserver_client::mgmt_api;
|
||||||
@@ -30,7 +30,7 @@ use utils::{
|
|||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::local_env::PageServerConf;
|
use crate::local_env::{NeonLocalInitPageserverConf, PageServerConf};
|
||||||
use crate::{background_process, local_env::LocalEnv};
|
use crate::{background_process, local_env::LocalEnv};
|
||||||
|
|
||||||
/// Directory within .neon which will be used by default for LocalFs remote storage.
|
/// Directory within .neon which will be used by default for LocalFs remote storage.
|
||||||
@@ -74,57 +74,23 @@ impl PageServerNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge overrides provided by the user on the command line with our default overides derived from neon_local configuration.
|
fn pageserver_init_make_toml(
|
||||||
///
|
&self,
|
||||||
/// These all end up on the command line of the `pageserver` binary.
|
conf: NeonLocalInitPageserverConf,
|
||||||
fn neon_local_overrides(&self, cli_overrides: &[&str]) -> Vec<String> {
|
) -> anyhow::Result<toml_edit::Document> {
|
||||||
|
assert_eq!(&PageServerConf::from(&conf), &self.conf, "during neon_local init, we derive the runtime state of ps conf (self.conf) from the --config flag fully");
|
||||||
|
|
||||||
|
// TODO(christian): instead of what we do here, create a pageserver_api::config::ConfigToml (PR #7656)
|
||||||
|
|
||||||
// FIXME: the paths should be shell-escaped to handle paths with spaces, quotas etc.
|
// FIXME: the paths should be shell-escaped to handle paths with spaces, quotas etc.
|
||||||
let pg_distrib_dir_param = format!(
|
let pg_distrib_dir_param = format!(
|
||||||
"pg_distrib_dir='{}'",
|
"pg_distrib_dir='{}'",
|
||||||
self.env.pg_distrib_dir_raw().display()
|
self.env.pg_distrib_dir_raw().display()
|
||||||
);
|
);
|
||||||
|
|
||||||
let PageServerConf {
|
|
||||||
id,
|
|
||||||
listen_pg_addr,
|
|
||||||
listen_http_addr,
|
|
||||||
pg_auth_type,
|
|
||||||
http_auth_type,
|
|
||||||
virtual_file_io_engine,
|
|
||||||
get_vectored_impl,
|
|
||||||
} = &self.conf;
|
|
||||||
|
|
||||||
let id = format!("id={}", id);
|
|
||||||
|
|
||||||
let http_auth_type_param = format!("http_auth_type='{}'", http_auth_type);
|
|
||||||
let listen_http_addr_param = format!("listen_http_addr='{}'", listen_http_addr);
|
|
||||||
|
|
||||||
let pg_auth_type_param = format!("pg_auth_type='{}'", pg_auth_type);
|
|
||||||
let listen_pg_addr_param = format!("listen_pg_addr='{}'", listen_pg_addr);
|
|
||||||
let virtual_file_io_engine = if let Some(virtual_file_io_engine) = virtual_file_io_engine {
|
|
||||||
format!("virtual_file_io_engine='{virtual_file_io_engine}'")
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
let get_vectored_impl = if let Some(get_vectored_impl) = get_vectored_impl {
|
|
||||||
format!("get_vectored_impl='{get_vectored_impl}'")
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let broker_endpoint_param = format!("broker_endpoint='{}'", self.env.broker.client_url());
|
let broker_endpoint_param = format!("broker_endpoint='{}'", self.env.broker.client_url());
|
||||||
|
|
||||||
let mut overrides = vec![
|
let mut overrides = vec![pg_distrib_dir_param, broker_endpoint_param];
|
||||||
id,
|
|
||||||
pg_distrib_dir_param,
|
|
||||||
http_auth_type_param,
|
|
||||||
pg_auth_type_param,
|
|
||||||
listen_http_addr_param,
|
|
||||||
listen_pg_addr_param,
|
|
||||||
broker_endpoint_param,
|
|
||||||
virtual_file_io_engine,
|
|
||||||
get_vectored_impl,
|
|
||||||
];
|
|
||||||
|
|
||||||
if let Some(control_plane_api) = &self.env.control_plane_api {
|
if let Some(control_plane_api) = &self.env.control_plane_api {
|
||||||
overrides.push(format!(
|
overrides.push(format!(
|
||||||
@@ -134,7 +100,7 @@ impl PageServerNode {
|
|||||||
|
|
||||||
// Storage controller uses the same auth as pageserver: if JWT is enabled
|
// Storage controller uses the same auth as pageserver: if JWT is enabled
|
||||||
// for us, we will also need it to talk to them.
|
// for us, we will also need it to talk to them.
|
||||||
if matches!(http_auth_type, AuthType::NeonJWT) {
|
if matches!(conf.http_auth_type, AuthType::NeonJWT) {
|
||||||
let jwt_token = self
|
let jwt_token = self
|
||||||
.env
|
.env
|
||||||
.generate_auth_token(&Claims::new(None, Scope::GenerationsApi))
|
.generate_auth_token(&Claims::new(None, Scope::GenerationsApi))
|
||||||
@@ -143,31 +109,40 @@ impl PageServerNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cli_overrides
|
if !conf.other.contains_key("remote_storage") {
|
||||||
.iter()
|
|
||||||
.any(|c| c.starts_with("remote_storage"))
|
|
||||||
{
|
|
||||||
overrides.push(format!(
|
overrides.push(format!(
|
||||||
"remote_storage={{local_path='../{PAGESERVER_REMOTE_STORAGE_DIR}'}}"
|
"remote_storage={{local_path='../{PAGESERVER_REMOTE_STORAGE_DIR}'}}"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if *http_auth_type != AuthType::Trust || *pg_auth_type != AuthType::Trust {
|
if conf.http_auth_type != AuthType::Trust || conf.pg_auth_type != AuthType::Trust {
|
||||||
// Keys are generated in the toplevel repo dir, pageservers' workdirs
|
// Keys are generated in the toplevel repo dir, pageservers' workdirs
|
||||||
// are one level below that, so refer to keys with ../
|
// are one level below that, so refer to keys with ../
|
||||||
overrides.push("auth_validation_public_key_path='../auth_public_key.pem'".to_owned());
|
overrides.push("auth_validation_public_key_path='../auth_public_key.pem'".to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply the user-provided overrides
|
// Apply the user-provided overrides
|
||||||
overrides.extend(cli_overrides.iter().map(|&c| c.to_owned()));
|
overrides.push(
|
||||||
|
toml_edit::ser::to_string_pretty(&conf)
|
||||||
|
.expect("we deserialized this from toml earlier"),
|
||||||
|
);
|
||||||
|
|
||||||
overrides
|
// Turn `overrides` into a toml document.
|
||||||
|
// TODO: above code is legacy code, it should be refactored to use toml_edit directly.
|
||||||
|
let mut config_toml = toml_edit::Document::new();
|
||||||
|
for fragment_str in overrides {
|
||||||
|
let fragment = toml_edit::Document::from_str(&fragment_str)
|
||||||
|
.expect("all fragments in `overrides` are valid toml documents, this function controls that");
|
||||||
|
for (key, item) in fragment.iter() {
|
||||||
|
config_toml.insert(key, item.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(config_toml)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initializes a pageserver node by creating its config with the overrides provided.
|
/// Initializes a pageserver node by creating its config with the overrides provided.
|
||||||
pub fn initialize(&self, config_overrides: &[&str]) -> anyhow::Result<()> {
|
pub fn initialize(&self, conf: NeonLocalInitPageserverConf) -> anyhow::Result<()> {
|
||||||
// First, run `pageserver --init` and wait for it to write a config into FS and exit.
|
self.pageserver_init(conf)
|
||||||
self.pageserver_init(config_overrides)
|
|
||||||
.with_context(|| format!("Failed to run init for pageserver node {}", self.conf.id))
|
.with_context(|| format!("Failed to run init for pageserver node {}", self.conf.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,11 +158,11 @@ impl PageServerNode {
|
|||||||
.expect("non-Unicode path")
|
.expect("non-Unicode path")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self, config_overrides: &[&str]) -> anyhow::Result<()> {
|
pub async fn start(&self, retry_timeout: &Duration) -> anyhow::Result<()> {
|
||||||
self.start_node(config_overrides, false).await
|
self.start_node(retry_timeout).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageserver_init(&self, config_overrides: &[&str]) -> anyhow::Result<()> {
|
fn pageserver_init(&self, conf: NeonLocalInitPageserverConf) -> anyhow::Result<()> {
|
||||||
let datadir = self.repo_path();
|
let datadir = self.repo_path();
|
||||||
let node_id = self.conf.id;
|
let node_id = self.conf.id;
|
||||||
println!(
|
println!(
|
||||||
@@ -198,29 +173,20 @@ impl PageServerNode {
|
|||||||
);
|
);
|
||||||
io::stdout().flush()?;
|
io::stdout().flush()?;
|
||||||
|
|
||||||
if !datadir.exists() {
|
let config = self
|
||||||
std::fs::create_dir(&datadir)?;
|
.pageserver_init_make_toml(conf)
|
||||||
}
|
.context("make pageserver toml")?;
|
||||||
|
let config_file_path = datadir.join("pageserver.toml");
|
||||||
let datadir_path_str = datadir.to_str().with_context(|| {
|
let mut config_file = std::fs::OpenOptions::new()
|
||||||
format!("Cannot start pageserver node {node_id} in path that has no string representation: {datadir:?}")
|
.create_new(true)
|
||||||
})?;
|
.write(true)
|
||||||
let mut args = self.pageserver_basic_args(config_overrides, datadir_path_str);
|
.open(&config_file_path)
|
||||||
args.push(Cow::Borrowed("--init"));
|
.with_context(|| format!("open pageserver toml for write: {config_file_path:?}"))?;
|
||||||
|
config_file
|
||||||
let init_output = Command::new(self.env.pageserver_bin())
|
.write_all(config.to_string().as_bytes())
|
||||||
.args(args.iter().map(Cow::as_ref))
|
.context("write pageserver toml")?;
|
||||||
.envs(self.pageserver_env_variables()?)
|
drop(config_file);
|
||||||
.output()
|
// TODO: invoke a TBD config-check command to validate that pageserver will start with the written config
|
||||||
.with_context(|| format!("Failed to run pageserver init for node {node_id}"))?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
init_output.status.success(),
|
|
||||||
"Pageserver init for node {} did not finish successfully, stdout: {}, stderr: {}",
|
|
||||||
node_id,
|
|
||||||
String::from_utf8_lossy(&init_output.stdout),
|
|
||||||
String::from_utf8_lossy(&init_output.stderr),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Write metadata file, used by pageserver on startup to register itself with
|
// Write metadata file, used by pageserver on startup to register itself with
|
||||||
// the storage controller
|
// the storage controller
|
||||||
@@ -234,12 +200,13 @@ impl PageServerNode {
|
|||||||
// situation: the metadata is written by some other script.
|
// situation: the metadata is written by some other script.
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
metadata_path,
|
metadata_path,
|
||||||
serde_json::to_vec(&serde_json::json!({
|
serde_json::to_vec(&pageserver_api::config::NodeMetadata {
|
||||||
"host": "localhost",
|
postgres_host: "localhost".to_string(),
|
||||||
"port": self.pg_connection_config.port(),
|
postgres_port: self.pg_connection_config.port(),
|
||||||
"http_host": "localhost",
|
http_host: "localhost".to_string(),
|
||||||
"http_port": http_port,
|
http_port,
|
||||||
}))
|
other: HashMap::new(),
|
||||||
|
})
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
)
|
)
|
||||||
.expect("Failed to write metadata file");
|
.expect("Failed to write metadata file");
|
||||||
@@ -247,18 +214,15 @@ impl PageServerNode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_node(
|
async fn start_node(&self, retry_timeout: &Duration) -> anyhow::Result<()> {
|
||||||
&self,
|
|
||||||
config_overrides: &[&str],
|
|
||||||
update_config: bool,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
// TODO: using a thread here because start_process() is not async but we need to call check_status()
|
// TODO: using a thread here because start_process() is not async but we need to call check_status()
|
||||||
let datadir = self.repo_path();
|
let datadir = self.repo_path();
|
||||||
print!(
|
print!(
|
||||||
"Starting pageserver node {} at '{}' in {:?}",
|
"Starting pageserver node {} at '{}' in {:?}, retrying for {:?}",
|
||||||
self.conf.id,
|
self.conf.id,
|
||||||
self.pg_connection_config.raw_address(),
|
self.pg_connection_config.raw_address(),
|
||||||
datadir
|
datadir,
|
||||||
|
retry_timeout
|
||||||
);
|
);
|
||||||
io::stdout().flush().context("flush stdout")?;
|
io::stdout().flush().context("flush stdout")?;
|
||||||
|
|
||||||
@@ -268,17 +232,15 @@ impl PageServerNode {
|
|||||||
self.conf.id, datadir,
|
self.conf.id, datadir,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let mut args = self.pageserver_basic_args(config_overrides, datadir_path_str);
|
let args = vec!["-D", datadir_path_str];
|
||||||
if update_config {
|
|
||||||
args.push(Cow::Borrowed("--update-config"));
|
|
||||||
}
|
|
||||||
background_process::start_process(
|
background_process::start_process(
|
||||||
"pageserver",
|
"pageserver",
|
||||||
&datadir,
|
&datadir,
|
||||||
&self.env.pageserver_bin(),
|
&self.env.pageserver_bin(),
|
||||||
args.iter().map(Cow::as_ref),
|
args,
|
||||||
self.pageserver_env_variables()?,
|
self.pageserver_env_variables()?,
|
||||||
background_process::InitialPidFile::Expect(self.pid_file()),
|
background_process::InitialPidFile::Expect(self.pid_file()),
|
||||||
|
retry_timeout,
|
||||||
|| async {
|
|| async {
|
||||||
let st = self.check_status().await;
|
let st = self.check_status().await;
|
||||||
match st {
|
match st {
|
||||||
@@ -293,22 +255,6 @@ impl PageServerNode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn pageserver_basic_args<'a>(
|
|
||||||
&self,
|
|
||||||
config_overrides: &'a [&'a str],
|
|
||||||
datadir_path_str: &'a str,
|
|
||||||
) -> Vec<Cow<'a, str>> {
|
|
||||||
let mut args = vec![Cow::Borrowed("-D"), Cow::Borrowed(datadir_path_str)];
|
|
||||||
|
|
||||||
let overrides = self.neon_local_overrides(config_overrides);
|
|
||||||
for config_override in overrides {
|
|
||||||
args.push(Cow::Borrowed("-c"));
|
|
||||||
args.push(Cow::Owned(config_override));
|
|
||||||
}
|
|
||||||
|
|
||||||
args
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pageserver_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> {
|
fn pageserver_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> {
|
||||||
// FIXME: why is this tied to pageserver's auth type? Whether or not the safekeeper
|
// FIXME: why is this tied to pageserver's auth type? Whether or not the safekeeper
|
||||||
// needs a token, and how to generate that token, seems independent to whether
|
// needs a token, and how to generate that token, seems independent to whether
|
||||||
@@ -389,6 +335,10 @@ impl PageServerNode {
|
|||||||
.remove("image_creation_threshold")
|
.remove("image_creation_threshold")
|
||||||
.map(|x| x.parse::<usize>())
|
.map(|x| x.parse::<usize>())
|
||||||
.transpose()?,
|
.transpose()?,
|
||||||
|
image_layer_creation_check_threshold: settings
|
||||||
|
.remove("image_layer_creation_check_threshold")
|
||||||
|
.map(|x| x.parse::<u8>())
|
||||||
|
.transpose()?,
|
||||||
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
||||||
walreceiver_connect_timeout: settings
|
walreceiver_connect_timeout: settings
|
||||||
.remove("walreceiver_connect_timeout")
|
.remove("walreceiver_connect_timeout")
|
||||||
@@ -430,6 +380,15 @@ impl PageServerNode {
|
|||||||
.map(serde_json::from_str)
|
.map(serde_json::from_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("parse `timeline_get_throttle` from json")?,
|
.context("parse `timeline_get_throttle` from json")?,
|
||||||
|
switch_aux_file_policy: settings
|
||||||
|
.remove("switch_aux_file_policy")
|
||||||
|
.map(|x| x.parse::<AuxFilePolicy>())
|
||||||
|
.transpose()
|
||||||
|
.context("Failed to parse 'switch_aux_file_policy'")?,
|
||||||
|
lsn_lease_length: settings.remove("lsn_lease_length").map(|x| x.to_string()),
|
||||||
|
lsn_lease_length_for_ts: settings
|
||||||
|
.remove("lsn_lease_length_for_ts")
|
||||||
|
.map(|x| x.to_string()),
|
||||||
};
|
};
|
||||||
if !settings.is_empty() {
|
if !settings.is_empty() {
|
||||||
bail!("Unrecognized tenant settings: {settings:?}")
|
bail!("Unrecognized tenant settings: {settings:?}")
|
||||||
@@ -501,6 +460,12 @@ impl PageServerNode {
|
|||||||
.map(|x| x.parse::<usize>())
|
.map(|x| x.parse::<usize>())
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("Failed to parse 'image_creation_threshold' as non zero integer")?,
|
.context("Failed to parse 'image_creation_threshold' as non zero integer")?,
|
||||||
|
image_layer_creation_check_threshold: settings
|
||||||
|
.remove("image_layer_creation_check_threshold")
|
||||||
|
.map(|x| x.parse::<u8>())
|
||||||
|
.transpose()
|
||||||
|
.context("Failed to parse 'image_creation_check_threshold' as integer")?,
|
||||||
|
|
||||||
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
pitr_interval: settings.remove("pitr_interval").map(|x| x.to_string()),
|
||||||
walreceiver_connect_timeout: settings
|
walreceiver_connect_timeout: settings
|
||||||
.remove("walreceiver_connect_timeout")
|
.remove("walreceiver_connect_timeout")
|
||||||
@@ -542,6 +507,15 @@ impl PageServerNode {
|
|||||||
.map(serde_json::from_str)
|
.map(serde_json::from_str)
|
||||||
.transpose()
|
.transpose()
|
||||||
.context("parse `timeline_get_throttle` from json")?,
|
.context("parse `timeline_get_throttle` from json")?,
|
||||||
|
switch_aux_file_policy: settings
|
||||||
|
.remove("switch_aux_file_policy")
|
||||||
|
.map(|x| x.parse::<AuxFilePolicy>())
|
||||||
|
.transpose()
|
||||||
|
.context("Failed to parse 'switch_aux_file_policy'")?,
|
||||||
|
lsn_lease_length: settings.remove("lsn_lease_length").map(|x| x.to_string()),
|
||||||
|
lsn_lease_length_for_ts: settings
|
||||||
|
.remove("lsn_lease_length_for_ts")
|
||||||
|
.map(|x| x.to_string()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//! ```
|
//! ```
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
use std::time::Duration;
|
||||||
use std::{io, result};
|
use std::{io, result};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
@@ -14,6 +15,7 @@ use camino::Utf8PathBuf;
|
|||||||
use postgres_connection::PgConnectionConfig;
|
use postgres_connection::PgConnectionConfig;
|
||||||
use reqwest::{IntoUrl, Method};
|
use reqwest::{IntoUrl, Method};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use utils::auth::{Claims, Scope};
|
||||||
use utils::{http::error::HttpErrorBody, id::NodeId};
|
use utils::{http::error::HttpErrorBody, id::NodeId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -70,24 +72,31 @@ pub struct SafekeeperNode {
|
|||||||
pub pg_connection_config: PgConnectionConfig,
|
pub pg_connection_config: PgConnectionConfig,
|
||||||
pub env: LocalEnv,
|
pub env: LocalEnv,
|
||||||
pub http_client: reqwest::Client,
|
pub http_client: reqwest::Client,
|
||||||
|
pub listen_addr: String,
|
||||||
pub http_base_url: String,
|
pub http_base_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SafekeeperNode {
|
impl SafekeeperNode {
|
||||||
pub fn from_env(env: &LocalEnv, conf: &SafekeeperConf) -> SafekeeperNode {
|
pub fn from_env(env: &LocalEnv, conf: &SafekeeperConf) -> SafekeeperNode {
|
||||||
|
let listen_addr = if let Some(ref listen_addr) = conf.listen_addr {
|
||||||
|
listen_addr.clone()
|
||||||
|
} else {
|
||||||
|
"127.0.0.1".to_string()
|
||||||
|
};
|
||||||
SafekeeperNode {
|
SafekeeperNode {
|
||||||
id: conf.id,
|
id: conf.id,
|
||||||
conf: conf.clone(),
|
conf: conf.clone(),
|
||||||
pg_connection_config: Self::safekeeper_connection_config(conf.pg_port),
|
pg_connection_config: Self::safekeeper_connection_config(&listen_addr, conf.pg_port),
|
||||||
env: env.clone(),
|
env: env.clone(),
|
||||||
http_client: reqwest::Client::new(),
|
http_client: reqwest::Client::new(),
|
||||||
http_base_url: format!("http://127.0.0.1:{}/v1", conf.http_port),
|
http_base_url: format!("http://{}:{}/v1", listen_addr, conf.http_port),
|
||||||
|
listen_addr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construct libpq connection string for connecting to this safekeeper.
|
/// Construct libpq connection string for connecting to this safekeeper.
|
||||||
fn safekeeper_connection_config(port: u16) -> PgConnectionConfig {
|
fn safekeeper_connection_config(addr: &str, port: u16) -> PgConnectionConfig {
|
||||||
PgConnectionConfig::new_host_port(url::Host::parse("127.0.0.1").unwrap(), port)
|
PgConnectionConfig::new_host_port(url::Host::parse(addr).unwrap(), port)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn datadir_path_by_id(env: &LocalEnv, sk_id: NodeId) -> PathBuf {
|
pub fn datadir_path_by_id(env: &LocalEnv, sk_id: NodeId) -> PathBuf {
|
||||||
@@ -103,16 +112,21 @@ impl SafekeeperNode {
|
|||||||
.expect("non-Unicode path")
|
.expect("non-Unicode path")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self, extra_opts: Vec<String>) -> anyhow::Result<()> {
|
pub async fn start(
|
||||||
|
&self,
|
||||||
|
extra_opts: Vec<String>,
|
||||||
|
retry_timeout: &Duration,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
print!(
|
print!(
|
||||||
"Starting safekeeper at '{}' in '{}'",
|
"Starting safekeeper at '{}' in '{}', retrying for {:?}",
|
||||||
self.pg_connection_config.raw_address(),
|
self.pg_connection_config.raw_address(),
|
||||||
self.datadir_path().display()
|
self.datadir_path().display(),
|
||||||
|
retry_timeout,
|
||||||
);
|
);
|
||||||
io::stdout().flush().unwrap();
|
io::stdout().flush().unwrap();
|
||||||
|
|
||||||
let listen_pg = format!("127.0.0.1:{}", self.conf.pg_port);
|
let listen_pg = format!("{}:{}", self.listen_addr, self.conf.pg_port);
|
||||||
let listen_http = format!("127.0.0.1:{}", self.conf.http_port);
|
let listen_http = format!("{}:{}", self.listen_addr, self.conf.http_port);
|
||||||
let id = self.id;
|
let id = self.id;
|
||||||
let datadir = self.datadir_path();
|
let datadir = self.datadir_path();
|
||||||
|
|
||||||
@@ -139,7 +153,7 @@ impl SafekeeperNode {
|
|||||||
availability_zone,
|
availability_zone,
|
||||||
];
|
];
|
||||||
if let Some(pg_tenant_only_port) = self.conf.pg_tenant_only_port {
|
if let Some(pg_tenant_only_port) = self.conf.pg_tenant_only_port {
|
||||||
let listen_pg_tenant_only = format!("127.0.0.1:{}", pg_tenant_only_port);
|
let listen_pg_tenant_only = format!("{}:{}", self.listen_addr, pg_tenant_only_port);
|
||||||
args.extend(["--listen-pg-tenant-only".to_owned(), listen_pg_tenant_only]);
|
args.extend(["--listen-pg-tenant-only".to_owned(), listen_pg_tenant_only]);
|
||||||
}
|
}
|
||||||
if !self.conf.sync {
|
if !self.conf.sync {
|
||||||
@@ -190,8 +204,9 @@ impl SafekeeperNode {
|
|||||||
&datadir,
|
&datadir,
|
||||||
&self.env.safekeeper_bin(),
|
&self.env.safekeeper_bin(),
|
||||||
&args,
|
&args,
|
||||||
[],
|
self.safekeeper_env_variables()?,
|
||||||
background_process::InitialPidFile::Expect(self.pid_file()),
|
background_process::InitialPidFile::Expect(self.pid_file()),
|
||||||
|
retry_timeout,
|
||||||
|| async {
|
|| async {
|
||||||
match self.check_status().await {
|
match self.check_status().await {
|
||||||
Ok(()) => Ok(true),
|
Ok(()) => Ok(true),
|
||||||
@@ -203,6 +218,18 @@ impl SafekeeperNode {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn safekeeper_env_variables(&self) -> anyhow::Result<Vec<(String, String)>> {
|
||||||
|
// Generate a token to connect from safekeeper to peers
|
||||||
|
if self.conf.auth_enabled {
|
||||||
|
let token = self
|
||||||
|
.env
|
||||||
|
.generate_auth_token(&Claims::new(None, Scope::SafekeeperData))?;
|
||||||
|
Ok(vec![("SAFEKEEPER_AUTH_TOKEN".to_owned(), token)])
|
||||||
|
} else {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Stop the server.
|
/// Stop the server.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use crate::{background_process, local_env::LocalEnv};
|
use crate::{
|
||||||
|
background_process,
|
||||||
|
local_env::{LocalEnv, NeonStorageControllerConf},
|
||||||
|
};
|
||||||
use camino::{Utf8Path, Utf8PathBuf};
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
use hyper::Method;
|
|
||||||
use pageserver_api::{
|
use pageserver_api::{
|
||||||
controller_api::{
|
controller_api::{
|
||||||
NodeConfigureRequest, NodeRegisterRequest, TenantCreateResponse, TenantLocateResponse,
|
NodeConfigureRequest, NodeRegisterRequest, TenantCreateResponse, TenantLocateResponse,
|
||||||
@@ -14,8 +16,9 @@ use pageserver_api::{
|
|||||||
};
|
};
|
||||||
use pageserver_client::mgmt_api::ResponseErrorMessageExt;
|
use pageserver_client::mgmt_api::ResponseErrorMessageExt;
|
||||||
use postgres_backend::AuthType;
|
use postgres_backend::AuthType;
|
||||||
|
use reqwest::Method;
|
||||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||||
use std::{fs, str::FromStr};
|
use std::{fs, str::FromStr, time::Duration};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
@@ -32,19 +35,18 @@ pub struct StorageController {
|
|||||||
public_key: Option<String>,
|
public_key: Option<String>,
|
||||||
postgres_port: u16,
|
postgres_port: u16,
|
||||||
client: reqwest::Client,
|
client: reqwest::Client,
|
||||||
|
config: NeonStorageControllerConf,
|
||||||
}
|
}
|
||||||
|
|
||||||
const COMMAND: &str = "storage_controller";
|
const COMMAND: &str = "storage_controller";
|
||||||
|
|
||||||
const STORAGE_CONTROLLER_POSTGRES_VERSION: u32 = 16;
|
const STORAGE_CONTROLLER_POSTGRES_VERSION: u32 = 16;
|
||||||
|
|
||||||
// Use a shorter pageserver unavailability interval than the default to speed up tests.
|
|
||||||
const NEON_LOCAL_MAX_UNAVAILABLE_INTERVAL: std::time::Duration = std::time::Duration::from_secs(10);
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct AttachHookRequest {
|
pub struct AttachHookRequest {
|
||||||
pub tenant_shard_id: TenantShardId,
|
pub tenant_shard_id: TenantShardId,
|
||||||
pub node_id: Option<NodeId>,
|
pub node_id: Option<NodeId>,
|
||||||
|
pub generation_override: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -135,6 +137,7 @@ impl StorageController {
|
|||||||
client: reqwest::ClientBuilder::new()
|
client: reqwest::ClientBuilder::new()
|
||||||
.build()
|
.build()
|
||||||
.expect("Failed to construct http client"),
|
.expect("Failed to construct http client"),
|
||||||
|
config: env.storage_controller.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,7 +224,7 @@ impl StorageController {
|
|||||||
Ok(database_url)
|
Ok(database_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(&self) -> anyhow::Result<()> {
|
pub async fn start(&self, retry_timeout: &Duration) -> anyhow::Result<()> {
|
||||||
// Start a vanilla Postgres process used by the storage controller for persistence.
|
// Start a vanilla Postgres process used by the storage controller for persistence.
|
||||||
let pg_data_path = Utf8PathBuf::from_path_buf(self.env.base_data_dir.clone())
|
let pg_data_path = Utf8PathBuf::from_path_buf(self.env.base_data_dir.clone())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -241,9 +244,13 @@ impl StorageController {
|
|||||||
anyhow::bail!("initdb failed with status {status}");
|
anyhow::bail!("initdb failed with status {status}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write a minimal config file:
|
||||||
|
// - Specify the port, since this is chosen dynamically
|
||||||
|
// - Switch off fsync, since we're running on lightweight test environments and when e.g. scale testing
|
||||||
|
// the storage controller we don't want a slow local disk to interfere with that.
|
||||||
tokio::fs::write(
|
tokio::fs::write(
|
||||||
&pg_data_path.join("postgresql.conf"),
|
&pg_data_path.join("postgresql.conf"),
|
||||||
format!("port = {}", self.postgres_port),
|
format!("port = {}\nfsync=off\n", self.postgres_port),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
};
|
};
|
||||||
@@ -265,6 +272,7 @@ impl StorageController {
|
|||||||
db_start_args,
|
db_start_args,
|
||||||
[],
|
[],
|
||||||
background_process::InitialPidFile::Create(self.postgres_pid_file()),
|
background_process::InitialPidFile::Create(self.postgres_pid_file()),
|
||||||
|
retry_timeout,
|
||||||
|| self.pg_isready(&pg_bin_dir),
|
|| self.pg_isready(&pg_bin_dir),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -272,8 +280,6 @@ impl StorageController {
|
|||||||
// Run migrations on every startup, in case something changed.
|
// Run migrations on every startup, in case something changed.
|
||||||
let database_url = self.setup_database().await?;
|
let database_url = self.setup_database().await?;
|
||||||
|
|
||||||
let max_unavailable: humantime::Duration = NEON_LOCAL_MAX_UNAVAILABLE_INTERVAL.into();
|
|
||||||
|
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"-l",
|
"-l",
|
||||||
&self.listen,
|
&self.listen,
|
||||||
@@ -283,7 +289,7 @@ impl StorageController {
|
|||||||
"--database-url",
|
"--database-url",
|
||||||
&database_url,
|
&database_url,
|
||||||
"--max-unavailable-interval",
|
"--max-unavailable-interval",
|
||||||
&max_unavailable.to_string(),
|
&humantime::Duration::from(self.config.max_unavailable).to_string(),
|
||||||
]
|
]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|s| s.to_string())
|
.map(|s| s.to_string())
|
||||||
@@ -305,16 +311,23 @@ impl StorageController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(split_threshold) = self.config.split_threshold.as_ref() {
|
||||||
|
args.push(format!("--split-threshold={split_threshold}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(format!(
|
||||||
|
"--neon-local-repo-dir={}",
|
||||||
|
self.env.base_data_dir.display()
|
||||||
|
));
|
||||||
|
|
||||||
background_process::start_process(
|
background_process::start_process(
|
||||||
COMMAND,
|
COMMAND,
|
||||||
&self.env.base_data_dir,
|
&self.env.base_data_dir,
|
||||||
&self.env.storage_controller_bin(),
|
&self.env.storage_controller_bin(),
|
||||||
args,
|
args,
|
||||||
[(
|
[],
|
||||||
"NEON_REPO_DIR".to_string(),
|
|
||||||
self.env.base_data_dir.to_string_lossy().to_string(),
|
|
||||||
)],
|
|
||||||
background_process::InitialPidFile::Create(self.pid_file()),
|
background_process::InitialPidFile::Create(self.pid_file()),
|
||||||
|
retry_timeout,
|
||||||
|| async {
|
|| async {
|
||||||
match self.ready().await {
|
match self.ready().await {
|
||||||
Ok(_) => Ok(true),
|
Ok(_) => Ok(true),
|
||||||
@@ -379,7 +392,7 @@ impl StorageController {
|
|||||||
/// Simple HTTP request wrapper for calling into storage controller
|
/// Simple HTTP request wrapper for calling into storage controller
|
||||||
async fn dispatch<RQ, RS>(
|
async fn dispatch<RQ, RS>(
|
||||||
&self,
|
&self,
|
||||||
method: hyper::Method,
|
method: reqwest::Method,
|
||||||
path: String,
|
path: String,
|
||||||
body: Option<RQ>,
|
body: Option<RQ>,
|
||||||
) -> anyhow::Result<RS>
|
) -> anyhow::Result<RS>
|
||||||
@@ -432,6 +445,7 @@ impl StorageController {
|
|||||||
let request = AttachHookRequest {
|
let request = AttachHookRequest {
|
||||||
tenant_shard_id,
|
tenant_shard_id,
|
||||||
node_id: Some(pageserver_id),
|
node_id: Some(pageserver_id),
|
||||||
|
generation_override: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = self
|
let response = self
|
||||||
@@ -472,6 +486,16 @@ impl StorageController {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn tenant_import(&self, tenant_id: TenantId) -> anyhow::Result<TenantCreateResponse> {
|
||||||
|
self.dispatch::<(), TenantCreateResponse>(
|
||||||
|
Method::POST,
|
||||||
|
format!("debug/v1/tenant/{tenant_id}/import"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn tenant_locate(&self, tenant_id: TenantId) -> anyhow::Result<TenantLocateResponse> {
|
pub async fn tenant_locate(&self, tenant_id: TenantId) -> anyhow::Result<TenantLocateResponse> {
|
||||||
self.dispatch::<(), _>(
|
self.dispatch::<(), _>(
|
||||||
|
|||||||
25
control_plane/storcon_cli/Cargo.toml
Normal file
25
control_plane/storcon_cli/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
[package]
|
||||||
|
name = "storcon_cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
comfy-table.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
|
humantime.workspace = true
|
||||||
|
hyper.workspace = true
|
||||||
|
pageserver_api.workspace = true
|
||||||
|
pageserver_client.workspace = true
|
||||||
|
reqwest.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
|
thiserror.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
utils.workspace = true
|
||||||
|
workspace_hack.workspace = true
|
||||||
|
|
||||||
860
control_plane/storcon_cli/src/main.rs
Normal file
860
control_plane/storcon_cli/src/main.rs
Normal file
@@ -0,0 +1,860 @@
|
|||||||
|
use futures::StreamExt;
|
||||||
|
use std::{str::FromStr, time::Duration};
|
||||||
|
|
||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use pageserver_api::{
|
||||||
|
controller_api::{
|
||||||
|
NodeAvailabilityWrapper, NodeDescribeResponse, ShardSchedulingPolicy,
|
||||||
|
TenantDescribeResponse, TenantPolicyRequest,
|
||||||
|
},
|
||||||
|
models::{
|
||||||
|
EvictionPolicy, EvictionPolicyLayerAccessThreshold, LocationConfigSecondary,
|
||||||
|
ShardParameters, TenantConfig, TenantConfigRequest, TenantCreateRequest,
|
||||||
|
TenantShardSplitRequest, TenantShardSplitResponse,
|
||||||
|
},
|
||||||
|
shard::{ShardStripeSize, TenantShardId},
|
||||||
|
};
|
||||||
|
use pageserver_client::mgmt_api::{self, ResponseErrorMessageExt};
|
||||||
|
use reqwest::{Method, StatusCode, Url};
|
||||||
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
use utils::id::{NodeId, TenantId};
|
||||||
|
|
||||||
|
use pageserver_api::controller_api::{
|
||||||
|
NodeConfigureRequest, NodeRegisterRequest, NodeSchedulingPolicy, PlacementPolicy,
|
||||||
|
TenantShardMigrateRequest, TenantShardMigrateResponse,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
enum Command {
|
||||||
|
/// Register a pageserver with the storage controller. This shouldn't usually be necessary,
|
||||||
|
/// since pageservers auto-register when they start up
|
||||||
|
NodeRegister {
|
||||||
|
#[arg(long)]
|
||||||
|
node_id: NodeId,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
listen_pg_addr: String,
|
||||||
|
#[arg(long)]
|
||||||
|
listen_pg_port: u16,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
listen_http_addr: String,
|
||||||
|
#[arg(long)]
|
||||||
|
listen_http_port: u16,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Modify a node's configuration in the storage controller
|
||||||
|
NodeConfigure {
|
||||||
|
#[arg(long)]
|
||||||
|
node_id: NodeId,
|
||||||
|
|
||||||
|
/// Availability is usually auto-detected based on heartbeats. Set 'offline' here to
|
||||||
|
/// manually mark a node offline
|
||||||
|
#[arg(long)]
|
||||||
|
availability: Option<NodeAvailabilityArg>,
|
||||||
|
/// Scheduling policy controls whether tenant shards may be scheduled onto this node.
|
||||||
|
#[arg(long)]
|
||||||
|
scheduling: Option<NodeSchedulingPolicy>,
|
||||||
|
},
|
||||||
|
/// Modify a tenant's policies in the storage controller
|
||||||
|
TenantPolicy {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
/// Placement policy controls whether a tenant is `detached`, has only a secondary location (`secondary`),
|
||||||
|
/// or is in the normal attached state with N secondary locations (`attached:N`)
|
||||||
|
#[arg(long)]
|
||||||
|
placement: Option<PlacementPolicyArg>,
|
||||||
|
/// Scheduling policy enables pausing the controller's scheduling activity involving this tenant. `active` is normal,
|
||||||
|
/// `essential` disables optimization scheduling changes, `pause` disables all scheduling changes, and `stop` prevents
|
||||||
|
/// all reconciliation activity including for scheduling changes already made. `pause` and `stop` can make a tenant
|
||||||
|
/// unavailable, and are only for use in emergencies.
|
||||||
|
#[arg(long)]
|
||||||
|
scheduling: Option<ShardSchedulingPolicyArg>,
|
||||||
|
},
|
||||||
|
/// List nodes known to the storage controller
|
||||||
|
Nodes {},
|
||||||
|
/// List tenants known to the storage controller
|
||||||
|
Tenants {},
|
||||||
|
/// Create a new tenant in the storage controller, and by extension on pageservers.
|
||||||
|
TenantCreate {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
},
|
||||||
|
/// Delete a tenant in the storage controller, and by extension on pageservers.
|
||||||
|
TenantDelete {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
},
|
||||||
|
/// Split an existing tenant into a higher number of shards than its current shard count.
|
||||||
|
TenantShardSplit {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
#[arg(long)]
|
||||||
|
shard_count: u8,
|
||||||
|
/// Optional, in 8kiB pages. e.g. set 2048 for 16MB stripes.
|
||||||
|
#[arg(long)]
|
||||||
|
stripe_size: Option<u32>,
|
||||||
|
},
|
||||||
|
/// Migrate the attached location for a tenant shard to a specific pageserver.
|
||||||
|
TenantShardMigrate {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_shard_id: TenantShardId,
|
||||||
|
#[arg(long)]
|
||||||
|
node: NodeId,
|
||||||
|
},
|
||||||
|
/// Modify the pageserver tenant configuration of a tenant: this is the configuration structure
|
||||||
|
/// that is passed through to pageservers, and does not affect storage controller behavior.
|
||||||
|
TenantConfig {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
#[arg(long)]
|
||||||
|
config: String,
|
||||||
|
},
|
||||||
|
/// Print details about a particular tenant, including all its shards' states.
|
||||||
|
TenantDescribe {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
},
|
||||||
|
/// For a tenant which hasn't been onboarded to the storage controller yet, add it in secondary
|
||||||
|
/// mode so that it can warm up content on a pageserver.
|
||||||
|
TenantWarmup {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
},
|
||||||
|
/// Uncleanly drop a tenant from the storage controller: this doesn't delete anything from pageservers. Appropriate
|
||||||
|
/// if you e.g. used `tenant-warmup` by mistake on a tenant ID that doesn't really exist, or is in some other region.
|
||||||
|
TenantDrop {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
#[arg(long)]
|
||||||
|
unclean: bool,
|
||||||
|
},
|
||||||
|
NodeDrop {
|
||||||
|
#[arg(long)]
|
||||||
|
node_id: NodeId,
|
||||||
|
#[arg(long)]
|
||||||
|
unclean: bool,
|
||||||
|
},
|
||||||
|
TenantSetTimeBasedEviction {
|
||||||
|
#[arg(long)]
|
||||||
|
tenant_id: TenantId,
|
||||||
|
#[arg(long)]
|
||||||
|
period: humantime::Duration,
|
||||||
|
#[arg(long)]
|
||||||
|
threshold: humantime::Duration,
|
||||||
|
},
|
||||||
|
// Drain a set of specified pageservers by moving the primary attachments to pageservers
|
||||||
|
// outside of the specified set.
|
||||||
|
Drain {
|
||||||
|
// Set of pageserver node ids to drain.
|
||||||
|
#[arg(long)]
|
||||||
|
nodes: Vec<NodeId>,
|
||||||
|
// Optional: migration concurrency (default is 8)
|
||||||
|
#[arg(long)]
|
||||||
|
concurrency: Option<usize>,
|
||||||
|
// Optional: maximum number of shards to migrate
|
||||||
|
#[arg(long)]
|
||||||
|
max_shards: Option<usize>,
|
||||||
|
// Optional: when set to true, nothing is migrated, but the plan is printed to stdout
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: Option<bool>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(
|
||||||
|
author,
|
||||||
|
version,
|
||||||
|
about,
|
||||||
|
long_about = "CLI for Storage Controller Support/Debug"
|
||||||
|
)]
|
||||||
|
#[command(arg_required_else_help(true))]
|
||||||
|
struct Cli {
|
||||||
|
#[arg(long)]
|
||||||
|
/// URL to storage controller. e.g. http://127.0.0.1:1234 when using `neon_local`
|
||||||
|
api: Url,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
/// JWT token for authenticating with storage controller. Depending on the API used, this
|
||||||
|
/// should have either `pageserverapi` or `admin` scopes: for convenience, you should mint
|
||||||
|
/// a token with both scopes to use with this tool.
|
||||||
|
jwt: Option<String>,
|
||||||
|
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct PlacementPolicyArg(PlacementPolicy);
|
||||||
|
|
||||||
|
impl FromStr for PlacementPolicyArg {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"detached" => Ok(Self(PlacementPolicy::Detached)),
|
||||||
|
"secondary" => Ok(Self(PlacementPolicy::Secondary)),
|
||||||
|
_ if s.starts_with("attached:") => {
|
||||||
|
let mut splitter = s.split(':');
|
||||||
|
let _prefix = splitter.next().unwrap();
|
||||||
|
match splitter.next().and_then(|s| s.parse::<usize>().ok()) {
|
||||||
|
Some(n) => Ok(Self(PlacementPolicy::Attached(n))),
|
||||||
|
None => Err(anyhow::anyhow!(
|
||||||
|
"Invalid format '{s}', a valid example is 'attached:1'"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Err(anyhow::anyhow!(
|
||||||
|
"Unknown placement policy '{s}', try detached,secondary,attached:<n>"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ShardSchedulingPolicyArg(ShardSchedulingPolicy);
|
||||||
|
|
||||||
|
impl FromStr for ShardSchedulingPolicyArg {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"active" => Ok(Self(ShardSchedulingPolicy::Active)),
|
||||||
|
"essential" => Ok(Self(ShardSchedulingPolicy::Essential)),
|
||||||
|
"pause" => Ok(Self(ShardSchedulingPolicy::Pause)),
|
||||||
|
"stop" => Ok(Self(ShardSchedulingPolicy::Stop)),
|
||||||
|
_ => Err(anyhow::anyhow!(
|
||||||
|
"Unknown scheduling policy '{s}', try active,essential,pause,stop"
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct NodeAvailabilityArg(NodeAvailabilityWrapper);
|
||||||
|
|
||||||
|
impl FromStr for NodeAvailabilityArg {
|
||||||
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
match s {
|
||||||
|
"active" => Ok(Self(NodeAvailabilityWrapper::Active)),
|
||||||
|
"offline" => Ok(Self(NodeAvailabilityWrapper::Offline)),
|
||||||
|
_ => Err(anyhow::anyhow!("Unknown availability state '{s}'")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Client {
|
||||||
|
base_url: Url,
|
||||||
|
jwt_token: Option<String>,
|
||||||
|
client: reqwest::Client,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Client {
|
||||||
|
fn new(base_url: Url, jwt_token: Option<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
base_url,
|
||||||
|
jwt_token,
|
||||||
|
client: reqwest::ClientBuilder::new()
|
||||||
|
.build()
|
||||||
|
.expect("Failed to construct http client"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple HTTP request wrapper for calling into storage controller
|
||||||
|
async fn dispatch<RQ, RS>(
|
||||||
|
&self,
|
||||||
|
method: Method,
|
||||||
|
path: String,
|
||||||
|
body: Option<RQ>,
|
||||||
|
) -> mgmt_api::Result<RS>
|
||||||
|
where
|
||||||
|
RQ: Serialize + Sized,
|
||||||
|
RS: DeserializeOwned + Sized,
|
||||||
|
{
|
||||||
|
// The configured URL has the /upcall path prefix for pageservers to use: we will strip that out
|
||||||
|
// for general purpose API access.
|
||||||
|
let url = Url::from_str(&format!(
|
||||||
|
"http://{}:{}/{path}",
|
||||||
|
self.base_url.host_str().unwrap(),
|
||||||
|
self.base_url.port().unwrap()
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut builder = self.client.request(method, url);
|
||||||
|
if let Some(body) = body {
|
||||||
|
builder = builder.json(&body)
|
||||||
|
}
|
||||||
|
if let Some(jwt_token) = &self.jwt_token {
|
||||||
|
builder = builder.header(
|
||||||
|
reqwest::header::AUTHORIZATION,
|
||||||
|
format!("Bearer {jwt_token}"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = builder.send().await.map_err(mgmt_api::Error::ReceiveBody)?;
|
||||||
|
let response = response.error_from_body().await?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.json()
|
||||||
|
.await
|
||||||
|
.map_err(pageserver_client::mgmt_api::Error::ReceiveBody)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let storcon_client = Client::new(cli.api.clone(), cli.jwt.clone());
|
||||||
|
|
||||||
|
let mut trimmed = cli.api.to_string();
|
||||||
|
trimmed.pop();
|
||||||
|
let vps_client = mgmt_api::Client::new(trimmed, cli.jwt.as_deref());
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Command::NodeRegister {
|
||||||
|
node_id,
|
||||||
|
listen_pg_addr,
|
||||||
|
listen_pg_port,
|
||||||
|
listen_http_addr,
|
||||||
|
listen_http_port,
|
||||||
|
} => {
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<_, ()>(
|
||||||
|
Method::POST,
|
||||||
|
"control/v1/node".to_string(),
|
||||||
|
Some(NodeRegisterRequest {
|
||||||
|
node_id,
|
||||||
|
listen_pg_addr,
|
||||||
|
listen_pg_port,
|
||||||
|
listen_http_addr,
|
||||||
|
listen_http_port,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::TenantCreate { tenant_id } => {
|
||||||
|
vps_client
|
||||||
|
.tenant_create(&TenantCreateRequest {
|
||||||
|
new_tenant_id: TenantShardId::unsharded(tenant_id),
|
||||||
|
generation: None,
|
||||||
|
shard_parameters: ShardParameters::default(),
|
||||||
|
placement_policy: Some(PlacementPolicy::Attached(1)),
|
||||||
|
config: TenantConfig::default(),
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::TenantDelete { tenant_id } => {
|
||||||
|
let status = vps_client
|
||||||
|
.tenant_delete(TenantShardId::unsharded(tenant_id))
|
||||||
|
.await?;
|
||||||
|
tracing::info!("Delete status: {}", status);
|
||||||
|
}
|
||||||
|
Command::Nodes {} => {
|
||||||
|
let resp = storcon_client
|
||||||
|
.dispatch::<(), Vec<NodeDescribeResponse>>(
|
||||||
|
Method::GET,
|
||||||
|
"control/v1/node".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut table = comfy_table::Table::new();
|
||||||
|
table.set_header(["Id", "Hostname", "Scheduling", "Availability"]);
|
||||||
|
for node in resp {
|
||||||
|
table.add_row([
|
||||||
|
format!("{}", node.id),
|
||||||
|
node.listen_http_addr,
|
||||||
|
format!("{:?}", node.scheduling),
|
||||||
|
format!("{:?}", node.availability),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
println!("{table}");
|
||||||
|
}
|
||||||
|
Command::NodeConfigure {
|
||||||
|
node_id,
|
||||||
|
availability,
|
||||||
|
scheduling,
|
||||||
|
} => {
|
||||||
|
let req = NodeConfigureRequest {
|
||||||
|
node_id,
|
||||||
|
availability: availability.map(|a| a.0),
|
||||||
|
scheduling,
|
||||||
|
};
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<_, ()>(
|
||||||
|
Method::PUT,
|
||||||
|
format!("control/v1/node/{node_id}/config"),
|
||||||
|
Some(req),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::Tenants {} => {
|
||||||
|
let resp = storcon_client
|
||||||
|
.dispatch::<(), Vec<TenantDescribeResponse>>(
|
||||||
|
Method::GET,
|
||||||
|
"control/v1/tenant".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let mut table = comfy_table::Table::new();
|
||||||
|
table.set_header([
|
||||||
|
"TenantId",
|
||||||
|
"ShardCount",
|
||||||
|
"StripeSize",
|
||||||
|
"Placement",
|
||||||
|
"Scheduling",
|
||||||
|
]);
|
||||||
|
for tenant in resp {
|
||||||
|
let shard_zero = tenant.shards.into_iter().next().unwrap();
|
||||||
|
table.add_row([
|
||||||
|
format!("{}", tenant.tenant_id),
|
||||||
|
format!("{}", shard_zero.tenant_shard_id.shard_count.literal()),
|
||||||
|
format!("{:?}", tenant.stripe_size),
|
||||||
|
format!("{:?}", tenant.policy),
|
||||||
|
format!("{:?}", shard_zero.scheduling_policy),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{table}");
|
||||||
|
}
|
||||||
|
Command::TenantPolicy {
|
||||||
|
tenant_id,
|
||||||
|
placement,
|
||||||
|
scheduling,
|
||||||
|
} => {
|
||||||
|
let req = TenantPolicyRequest {
|
||||||
|
scheduling: scheduling.map(|s| s.0),
|
||||||
|
placement: placement.map(|p| p.0),
|
||||||
|
};
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<_, ()>(
|
||||||
|
Method::PUT,
|
||||||
|
format!("control/v1/tenant/{tenant_id}/policy"),
|
||||||
|
Some(req),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::TenantShardSplit {
|
||||||
|
tenant_id,
|
||||||
|
shard_count,
|
||||||
|
stripe_size,
|
||||||
|
} => {
|
||||||
|
let req = TenantShardSplitRequest {
|
||||||
|
new_shard_count: shard_count,
|
||||||
|
new_stripe_size: stripe_size.map(ShardStripeSize),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = storcon_client
|
||||||
|
.dispatch::<TenantShardSplitRequest, TenantShardSplitResponse>(
|
||||||
|
Method::PUT,
|
||||||
|
format!("control/v1/tenant/{tenant_id}/shard_split"),
|
||||||
|
Some(req),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
println!(
|
||||||
|
"Split tenant {} into {} shards: {}",
|
||||||
|
tenant_id,
|
||||||
|
shard_count,
|
||||||
|
response
|
||||||
|
.new_shards
|
||||||
|
.iter()
|
||||||
|
.map(|s| format!("{:?}", s))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Command::TenantShardMigrate {
|
||||||
|
tenant_shard_id,
|
||||||
|
node,
|
||||||
|
} => {
|
||||||
|
let req = TenantShardMigrateRequest {
|
||||||
|
tenant_shard_id,
|
||||||
|
node_id: node,
|
||||||
|
};
|
||||||
|
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
|
||||||
|
Method::PUT,
|
||||||
|
format!("control/v1/tenant/{tenant_shard_id}/migrate"),
|
||||||
|
Some(req),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::TenantConfig { tenant_id, config } => {
|
||||||
|
let tenant_conf = serde_json::from_str(&config)?;
|
||||||
|
|
||||||
|
vps_client
|
||||||
|
.tenant_config(&TenantConfigRequest {
|
||||||
|
tenant_id,
|
||||||
|
config: tenant_conf,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::TenantDescribe { tenant_id } => {
|
||||||
|
let describe_response = storcon_client
|
||||||
|
.dispatch::<(), TenantDescribeResponse>(
|
||||||
|
Method::GET,
|
||||||
|
format!("control/v1/tenant/{tenant_id}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let shards = describe_response.shards;
|
||||||
|
let mut table = comfy_table::Table::new();
|
||||||
|
table.set_header(["Shard", "Attached", "Secondary", "Last error", "status"]);
|
||||||
|
for shard in shards {
|
||||||
|
let secondary = shard
|
||||||
|
.node_secondary
|
||||||
|
.iter()
|
||||||
|
.map(|n| format!("{}", n))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
let mut status_parts = Vec::new();
|
||||||
|
if shard.is_reconciling {
|
||||||
|
status_parts.push("reconciling");
|
||||||
|
}
|
||||||
|
|
||||||
|
if shard.is_pending_compute_notification {
|
||||||
|
status_parts.push("pending_compute");
|
||||||
|
}
|
||||||
|
|
||||||
|
if shard.is_splitting {
|
||||||
|
status_parts.push("splitting");
|
||||||
|
}
|
||||||
|
let status = status_parts.join(",");
|
||||||
|
|
||||||
|
table.add_row([
|
||||||
|
format!("{}", shard.tenant_shard_id),
|
||||||
|
shard
|
||||||
|
.node_attached
|
||||||
|
.map(|n| format!("{}", n))
|
||||||
|
.unwrap_or(String::new()),
|
||||||
|
secondary,
|
||||||
|
shard.last_error,
|
||||||
|
status,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
println!("{table}");
|
||||||
|
}
|
||||||
|
Command::TenantWarmup { tenant_id } => {
|
||||||
|
let describe_response = storcon_client
|
||||||
|
.dispatch::<(), TenantDescribeResponse>(
|
||||||
|
Method::GET,
|
||||||
|
format!("control/v1/tenant/{tenant_id}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match describe_response {
|
||||||
|
Ok(describe) => {
|
||||||
|
if matches!(describe.policy, PlacementPolicy::Secondary) {
|
||||||
|
// Fine: it's already known to controller in secondary mode: calling
|
||||||
|
// again to put it into secondary mode won't cause problems.
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Tenant already present with policy {:?}", describe.policy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(mgmt_api::Error::ApiError(StatusCode::NOT_FOUND, _)) => {
|
||||||
|
// Fine: this tenant isn't know to the storage controller yet.
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Unexpected API error
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vps_client
|
||||||
|
.location_config(
|
||||||
|
TenantShardId::unsharded(tenant_id),
|
||||||
|
pageserver_api::models::LocationConfig {
|
||||||
|
mode: pageserver_api::models::LocationConfigMode::Secondary,
|
||||||
|
generation: None,
|
||||||
|
secondary_conf: Some(LocationConfigSecondary { warm: true }),
|
||||||
|
shard_number: 0,
|
||||||
|
shard_count: 0,
|
||||||
|
shard_stripe_size: ShardParameters::DEFAULT_STRIPE_SIZE.0,
|
||||||
|
tenant_conf: TenantConfig::default(),
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let describe_response = storcon_client
|
||||||
|
.dispatch::<(), TenantDescribeResponse>(
|
||||||
|
Method::GET,
|
||||||
|
format!("control/v1/tenant/{tenant_id}"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let secondary_ps_id = describe_response
|
||||||
|
.shards
|
||||||
|
.first()
|
||||||
|
.unwrap()
|
||||||
|
.node_secondary
|
||||||
|
.first()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("Tenant {tenant_id} warming up on pageserver {secondary_ps_id}");
|
||||||
|
loop {
|
||||||
|
let (status, progress) = vps_client
|
||||||
|
.tenant_secondary_download(
|
||||||
|
TenantShardId::unsharded(tenant_id),
|
||||||
|
Some(Duration::from_secs(10)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
println!(
|
||||||
|
"Progress: {}/{} layers, {}/{} bytes",
|
||||||
|
progress.layers_downloaded,
|
||||||
|
progress.layers_total,
|
||||||
|
progress.bytes_downloaded,
|
||||||
|
progress.bytes_total
|
||||||
|
);
|
||||||
|
match status {
|
||||||
|
StatusCode::OK => {
|
||||||
|
println!("Download complete");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
StatusCode::ACCEPTED => {
|
||||||
|
// Loop
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
anyhow::bail!("Unexpected download status: {status}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::TenantDrop { tenant_id, unclean } => {
|
||||||
|
if !unclean {
|
||||||
|
anyhow::bail!("This command is not a tenant deletion, and uncleanly drops all controller state for the tenant. If you know what you're doing, add `--unclean` to proceed.")
|
||||||
|
}
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<(), ()>(
|
||||||
|
Method::POST,
|
||||||
|
format!("debug/v1/tenant/{tenant_id}/drop"),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::NodeDrop { node_id, unclean } => {
|
||||||
|
if !unclean {
|
||||||
|
anyhow::bail!("This command is not a clean node decommission, and uncleanly drops all controller state for the node, without checking if any tenants still refer to it. If you know what you're doing, add `--unclean` to proceed.")
|
||||||
|
}
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<(), ()>(Method::POST, format!("debug/v1/node/{node_id}/drop"), None)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::TenantSetTimeBasedEviction {
|
||||||
|
tenant_id,
|
||||||
|
period,
|
||||||
|
threshold,
|
||||||
|
} => {
|
||||||
|
vps_client
|
||||||
|
.tenant_config(&TenantConfigRequest {
|
||||||
|
tenant_id,
|
||||||
|
config: TenantConfig {
|
||||||
|
eviction_policy: Some(EvictionPolicy::LayerAccessThreshold(
|
||||||
|
EvictionPolicyLayerAccessThreshold {
|
||||||
|
period: period.into(),
|
||||||
|
threshold: threshold.into(),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Command::Drain {
|
||||||
|
nodes,
|
||||||
|
concurrency,
|
||||||
|
max_shards,
|
||||||
|
dry_run,
|
||||||
|
} => {
|
||||||
|
// Load the list of nodes, split them up into the drained and filled sets,
|
||||||
|
// and validate that draining is possible.
|
||||||
|
let node_descs = storcon_client
|
||||||
|
.dispatch::<(), Vec<NodeDescribeResponse>>(
|
||||||
|
Method::GET,
|
||||||
|
"control/v1/node".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut node_to_drain_descs = Vec::new();
|
||||||
|
let mut node_to_fill_descs = Vec::new();
|
||||||
|
|
||||||
|
for desc in node_descs {
|
||||||
|
let to_drain = nodes.iter().any(|id| *id == desc.id);
|
||||||
|
if to_drain {
|
||||||
|
node_to_drain_descs.push(desc);
|
||||||
|
} else {
|
||||||
|
node_to_fill_descs.push(desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodes.len() != node_to_drain_descs.len() {
|
||||||
|
anyhow::bail!("Drain requested for node which doesn't exist.")
|
||||||
|
}
|
||||||
|
|
||||||
|
node_to_fill_descs.retain(|desc| {
|
||||||
|
matches!(desc.availability, NodeAvailabilityWrapper::Active)
|
||||||
|
&& matches!(
|
||||||
|
desc.scheduling,
|
||||||
|
NodeSchedulingPolicy::Active | NodeSchedulingPolicy::Filling
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if node_to_fill_descs.is_empty() {
|
||||||
|
anyhow::bail!("There are no nodes to drain to")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the node scheduling policy to draining for the nodes which
|
||||||
|
// we plan to drain.
|
||||||
|
for node_desc in node_to_drain_descs.iter() {
|
||||||
|
let req = NodeConfigureRequest {
|
||||||
|
node_id: node_desc.id,
|
||||||
|
availability: None,
|
||||||
|
scheduling: Some(NodeSchedulingPolicy::Draining),
|
||||||
|
};
|
||||||
|
|
||||||
|
storcon_client
|
||||||
|
.dispatch::<_, ()>(
|
||||||
|
Method::PUT,
|
||||||
|
format!("control/v1/node/{}/config", node_desc.id),
|
||||||
|
Some(req),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the drain: move each tenant shard scheduled on a node to
|
||||||
|
// be drained to a node which is being filled. A simple round robin
|
||||||
|
// strategy is used to pick the new node.
|
||||||
|
let tenants = storcon_client
|
||||||
|
.dispatch::<(), Vec<TenantDescribeResponse>>(
|
||||||
|
Method::GET,
|
||||||
|
"control/v1/tenant".to_string(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let mut selected_node_idx = 0;
|
||||||
|
|
||||||
|
struct DrainMove {
|
||||||
|
tenant_shard_id: TenantShardId,
|
||||||
|
from: NodeId,
|
||||||
|
to: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut moves: Vec<DrainMove> = Vec::new();
|
||||||
|
|
||||||
|
let shards = tenants
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|tenant| tenant.shards.into_iter());
|
||||||
|
for shard in shards {
|
||||||
|
if let Some(max_shards) = max_shards {
|
||||||
|
if moves.len() >= max_shards {
|
||||||
|
println!(
|
||||||
|
"Stop planning shard moves since the requested maximum was reached"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_migrate = {
|
||||||
|
if let Some(attached_to) = shard.node_attached {
|
||||||
|
node_to_drain_descs
|
||||||
|
.iter()
|
||||||
|
.map(|desc| desc.id)
|
||||||
|
.any(|id| id == attached_to)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if !should_migrate {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
moves.push(DrainMove {
|
||||||
|
tenant_shard_id: shard.tenant_shard_id,
|
||||||
|
from: shard
|
||||||
|
.node_attached
|
||||||
|
.expect("We only migrate attached tenant shards"),
|
||||||
|
to: node_to_fill_descs[selected_node_idx].id,
|
||||||
|
});
|
||||||
|
selected_node_idx = (selected_node_idx + 1) % node_to_fill_descs.len();
|
||||||
|
}
|
||||||
|
|
||||||
|
let total_moves = moves.len();
|
||||||
|
|
||||||
|
if dry_run == Some(true) {
|
||||||
|
println!("Dryrun requested. Planned {total_moves} moves:");
|
||||||
|
for mv in &moves {
|
||||||
|
println!("{}: {} -> {}", mv.tenant_shard_id, mv.from, mv.to)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_MIGRATE_CONCURRENCY: usize = 8;
|
||||||
|
let mut stream = futures::stream::iter(moves)
|
||||||
|
.map(|mv| {
|
||||||
|
let client = Client::new(cli.api.clone(), cli.jwt.clone());
|
||||||
|
async move {
|
||||||
|
client
|
||||||
|
.dispatch::<TenantShardMigrateRequest, TenantShardMigrateResponse>(
|
||||||
|
Method::PUT,
|
||||||
|
format!("control/v1/tenant/{}/migrate", mv.tenant_shard_id),
|
||||||
|
Some(TenantShardMigrateRequest {
|
||||||
|
tenant_shard_id: mv.tenant_shard_id,
|
||||||
|
node_id: mv.to,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| (mv.tenant_shard_id, mv.from, mv.to, e))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.buffered(concurrency.unwrap_or(DEFAULT_MIGRATE_CONCURRENCY));
|
||||||
|
|
||||||
|
let mut success = 0;
|
||||||
|
let mut failure = 0;
|
||||||
|
|
||||||
|
while let Some(res) = stream.next().await {
|
||||||
|
match res {
|
||||||
|
Ok(_) => {
|
||||||
|
success += 1;
|
||||||
|
}
|
||||||
|
Err((tenant_shard_id, from, to, error)) => {
|
||||||
|
failure += 1;
|
||||||
|
println!(
|
||||||
|
"Failed to migrate {} from node {} to node {}: {}",
|
||||||
|
tenant_shard_id, from, to, error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (success + failure) % 20 == 0 {
|
||||||
|
println!(
|
||||||
|
"Processed {}/{} shards: {} succeeded, {} failed",
|
||||||
|
success + failure,
|
||||||
|
total_moves,
|
||||||
|
success,
|
||||||
|
failure
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Processed {}/{} shards: {} succeeded, {} failed",
|
||||||
|
success + failure,
|
||||||
|
total_moves,
|
||||||
|
success,
|
||||||
|
failure
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -99,6 +99,13 @@ name = "async-executor"
|
|||||||
[[bans.deny]]
|
[[bans.deny]]
|
||||||
name = "smol"
|
name = "smol"
|
||||||
|
|
||||||
|
[[bans.deny]]
|
||||||
|
# We want to use rustls instead of the platform's native tls implementation.
|
||||||
|
name = "native-tls"
|
||||||
|
|
||||||
|
[[bans.deny]]
|
||||||
|
name = "openssl"
|
||||||
|
|
||||||
# This section is considered when running `cargo deny check sources`.
|
# This section is considered when running `cargo deny check sources`.
|
||||||
# More documentation about the 'sources' section can be found here:
|
# More documentation about the 'sources' section can be found here:
|
||||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
# see https://diesel.rs/guides/configuring-diesel-cli
|
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
[print_schema]
|
[print_schema]
|
||||||
file = "control_plane/attachment_service/src/schema.rs"
|
file = "storage_controller/src/schema.rs"
|
||||||
custom_type_derives = ["diesel::query_builder::QueryId"]
|
custom_type_derives = ["diesel::query_builder::QueryId"]
|
||||||
|
|
||||||
[migrations_directory]
|
[migrations_directory]
|
||||||
dir = "control_plane/attachment_service/migrations"
|
dir = "storage_controller/migrations"
|
||||||
|
|||||||
10
docker-compose/README.md
Normal file
10
docker-compose/README.md
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
# Example docker compose configuration
|
||||||
|
|
||||||
|
The configuration in this directory is used for testing Neon docker images: it is
|
||||||
|
not intended for deploying a usable system. To run a development environment where
|
||||||
|
you can experiment with a minature Neon system, use `cargo neon` rather than container images.
|
||||||
|
|
||||||
|
This configuration does not start the storage controller, because the controller
|
||||||
|
needs a way to reconfigure running computes, and no such thing exists in this setup.
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG REPOSITORY=369495373322.dkr.ecr.eu-central-1.amazonaws.com
|
ARG REPOSITORY=neondatabase
|
||||||
ARG COMPUTE_IMAGE=compute-node-v14
|
ARG COMPUTE_IMAGE=compute-node-v14
|
||||||
ARG TAG=latest
|
ARG TAG=latest
|
||||||
|
|
||||||
@@ -8,6 +8,11 @@ USER root
|
|||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y curl \
|
apt-get install -y curl \
|
||||||
jq \
|
jq \
|
||||||
|
python3-pip \
|
||||||
netcat
|
netcat
|
||||||
|
#Faker is required for the pg_anon test
|
||||||
|
RUN pip3 install Faker
|
||||||
|
#This is required for the pg_hintplan test
|
||||||
|
RUN mkdir -p /ext-src/pg_hint_plan-src && chown postgres /ext-src/pg_hint_plan-src
|
||||||
|
|
||||||
USER postgres
|
USER postgres
|
||||||
@@ -23,11 +23,10 @@ echo "Page server is ready."
|
|||||||
echo "Create a tenant and timeline"
|
echo "Create a tenant and timeline"
|
||||||
generate_id tenant_id
|
generate_id tenant_id
|
||||||
PARAMS=(
|
PARAMS=(
|
||||||
-sb
|
-X PUT
|
||||||
-X POST
|
|
||||||
-H "Content-Type: application/json"
|
-H "Content-Type: application/json"
|
||||||
-d "{\"new_tenant_id\": \"${tenant_id}\"}"
|
-d "{\"mode\": \"AttachedSingle\", \"generation\": 1, \"tenant_conf\": {}}"
|
||||||
http://pageserver:9898/v1/tenant/
|
"http://pageserver:9898/v1/tenant/${tenant_id}/location_config"
|
||||||
)
|
)
|
||||||
result=$(curl "${PARAMS[@]}")
|
result=$(curl "${PARAMS[@]}")
|
||||||
echo $result | jq .
|
echo $result | jq .
|
||||||
|
|||||||
@@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "shared_preload_libraries",
|
"name": "shared_preload_libraries",
|
||||||
"value": "neon",
|
"value": "neon,pg_cron,timescaledb,pg_stat_statements",
|
||||||
"vartype": "string"
|
"vartype": "string"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -127,6 +127,16 @@
|
|||||||
"name": "max_replication_flush_lag",
|
"name": "max_replication_flush_lag",
|
||||||
"value": "10GB",
|
"value": "10GB",
|
||||||
"vartype": "string"
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cron.database",
|
||||||
|
"value": "postgres",
|
||||||
|
"vartype": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "session_preload_libraries",
|
||||||
|
"value": "anon",
|
||||||
|
"vartype": "string"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
minio:
|
minio:
|
||||||
restart: always
|
restart: always
|
||||||
@@ -161,12 +159,12 @@ services:
|
|||||||
context: ./compute_wrapper/
|
context: ./compute_wrapper/
|
||||||
args:
|
args:
|
||||||
- REPOSITORY=${REPOSITORY:-neondatabase}
|
- REPOSITORY=${REPOSITORY:-neondatabase}
|
||||||
- COMPUTE_IMAGE=compute-node-v${PG_VERSION:-14}
|
- COMPUTE_IMAGE=compute-node-v${PG_VERSION:-16}
|
||||||
- TAG=${TAG:-latest}
|
- TAG=${TAG:-latest}
|
||||||
- http_proxy=$http_proxy
|
- http_proxy=$http_proxy
|
||||||
- https_proxy=$https_proxy
|
- https_proxy=$https_proxy
|
||||||
environment:
|
environment:
|
||||||
- PG_VERSION=${PG_VERSION:-14}
|
- PG_VERSION=${PG_VERSION:-16}
|
||||||
#- RUST_BACKTRACE=1
|
#- RUST_BACKTRACE=1
|
||||||
# Mount the test files directly, for faster editing cycle.
|
# Mount the test files directly, for faster editing cycle.
|
||||||
volumes:
|
volumes:
|
||||||
@@ -194,3 +192,14 @@ services:
|
|||||||
done"
|
done"
|
||||||
depends_on:
|
depends_on:
|
||||||
- compute
|
- compute
|
||||||
|
|
||||||
|
neon-test-extensions:
|
||||||
|
profiles: ["test-extensions"]
|
||||||
|
image: ${REPOSITORY:-neondatabase}/neon-test-extensions-v${PG_TEST_VERSION:-16}:${TAG:-latest}
|
||||||
|
entrypoint:
|
||||||
|
- "/bin/bash"
|
||||||
|
- "-c"
|
||||||
|
command:
|
||||||
|
- sleep 1800
|
||||||
|
depends_on:
|
||||||
|
- compute
|
||||||
|
|||||||
@@ -7,54 +7,94 @@
|
|||||||
# Implicitly accepts `REPOSITORY` and `TAG` env vars that are passed into the compose file
|
# Implicitly accepts `REPOSITORY` and `TAG` env vars that are passed into the compose file
|
||||||
# Their defaults point at DockerHub `neondatabase/neon:latest` image.`,
|
# Their defaults point at DockerHub `neondatabase/neon:latest` image.`,
|
||||||
# to verify custom image builds (e.g pre-published ones).
|
# to verify custom image builds (e.g pre-published ones).
|
||||||
|
#
|
||||||
# XXX: Current does not work on M1 macs due to x86_64 Docker images compiled only, and no seccomp support in M1 Docker emulation layer.
|
# A test script for postgres extensions
|
||||||
|
# Currently supports only v16
|
||||||
|
#
|
||||||
set -eux -o pipefail
|
set -eux -o pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
|
COMPOSE_FILE='docker-compose.yml'
|
||||||
COMPOSE_FILE=$SCRIPT_DIR/docker-compose.yml
|
cd $(dirname $0)
|
||||||
|
|
||||||
COMPUTE_CONTAINER_NAME=docker-compose-compute-1
|
COMPUTE_CONTAINER_NAME=docker-compose-compute-1
|
||||||
SQL="CREATE TABLE t(key int primary key, value text); insert into t values(1,1); select * from t;"
|
TEST_CONTAINER_NAME=docker-compose-neon-test-extensions-1
|
||||||
PSQL_OPTION="-h localhost -U cloud_admin -p 55433 -c '$SQL' postgres"
|
PSQL_OPTION="-h localhost -U cloud_admin -p 55433 -d postgres"
|
||||||
|
: ${http_proxy:=}
|
||||||
|
: ${https_proxy:=}
|
||||||
|
export http_proxy https_proxy
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo "show container information"
|
echo "show container information"
|
||||||
docker ps
|
docker ps
|
||||||
docker compose -f $COMPOSE_FILE logs
|
docker compose --profile test-extensions -f $COMPOSE_FILE logs
|
||||||
echo "stop containers..."
|
echo "stop containers..."
|
||||||
docker compose -f $COMPOSE_FILE down
|
docker compose --profile test-extensions -f $COMPOSE_FILE down
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "clean up containers if exists"
|
|
||||||
cleanup
|
|
||||||
|
|
||||||
for pg_version in 14 15 16; do
|
for pg_version in 14 15 16; do
|
||||||
echo "start containers (pg_version=$pg_version)."
|
echo "clean up containers if exists"
|
||||||
PG_VERSION=$pg_version docker compose -f $COMPOSE_FILE up --build -d
|
cleanup
|
||||||
|
PG_TEST_VERSION=$(($pg_version < 16 ? 16 : $pg_version))
|
||||||
|
PG_VERSION=$pg_version PG_TEST_VERSION=$PG_TEST_VERSION docker compose --profile test-extensions -f $COMPOSE_FILE up --build -d
|
||||||
|
|
||||||
echo "wait until the compute is ready. timeout after 60s. "
|
echo "wait until the compute is ready. timeout after 60s. "
|
||||||
cnt=0
|
cnt=0
|
||||||
while sleep 1; do
|
while sleep 3; do
|
||||||
# check timeout
|
# check timeout
|
||||||
cnt=`expr $cnt + 1`
|
cnt=`expr $cnt + 3`
|
||||||
if [ $cnt -gt 60 ]; then
|
if [ $cnt -gt 60 ]; then
|
||||||
echo "timeout before the compute is ready."
|
echo "timeout before the compute is ready."
|
||||||
cleanup
|
cleanup
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
if docker compose --profile test-extensions -f $COMPOSE_FILE logs "compute_is_ready" | grep -q "accepting connections"; then
|
||||||
# check if the compute is ready
|
|
||||||
set +o pipefail
|
|
||||||
result=`docker compose -f $COMPOSE_FILE logs "compute_is_ready" | grep "accepting connections" | wc -l`
|
|
||||||
set -o pipefail
|
|
||||||
if [ $result -eq 1 ]; then
|
|
||||||
echo "OK. The compute is ready to connect."
|
echo "OK. The compute is ready to connect."
|
||||||
echo "execute simple queries."
|
echo "execute simple queries."
|
||||||
docker exec $COMPUTE_CONTAINER_NAME /bin/bash -c "psql $PSQL_OPTION"
|
docker exec $COMPUTE_CONTAINER_NAME /bin/bash -c "psql $PSQL_OPTION"
|
||||||
cleanup
|
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
if [ $pg_version -ge 16 ]
|
||||||
|
then
|
||||||
|
echo Enabling trust connection
|
||||||
|
docker exec $COMPUTE_CONTAINER_NAME bash -c "sed -i '\$d' /var/db/postgres/compute/pg_hba.conf && echo -e 'host\t all\t all\t all\t trust' >> /var/db/postgres/compute/pg_hba.conf && psql $PSQL_OPTION -c 'select pg_reload_conf()' "
|
||||||
|
echo Adding postgres role
|
||||||
|
docker exec $COMPUTE_CONTAINER_NAME psql $PSQL_OPTION -c "CREATE ROLE postgres SUPERUSER LOGIN"
|
||||||
|
# This is required for the pg_hint_plan test, to prevent flaky log message causing the test to fail
|
||||||
|
# It cannot be moved to Dockerfile now because the database directory is created after the start of the container
|
||||||
|
echo Adding dummy config
|
||||||
|
docker exec $COMPUTE_CONTAINER_NAME touch /var/db/postgres/compute/compute_ctl_temp_override.conf
|
||||||
|
# This block is required for the pg_anon extension test.
|
||||||
|
# The test assumes that it is running on the same host with the postgres engine.
|
||||||
|
# In our case it's not true, that's why we are copying files to the compute node
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
docker cp $TEST_CONTAINER_NAME:/ext-src/pg_anon-src/data $TMPDIR/data
|
||||||
|
echo -e '1\t too \t many \t tabs' > $TMPDIR/data/bad.csv
|
||||||
|
docker cp $TMPDIR/data $COMPUTE_CONTAINER_NAME:/tmp/tmp_anon_alternate_data
|
||||||
|
rm -rf $TMPDIR
|
||||||
|
TMPDIR=$(mktemp -d)
|
||||||
|
# The following block does the same for the pg_hintplan test
|
||||||
|
docker cp $TEST_CONTAINER_NAME:/ext-src/pg_hint_plan-src/data $TMPDIR/data
|
||||||
|
docker cp $TMPDIR/data $COMPUTE_CONTAINER_NAME:/ext-src/pg_hint_plan-src/
|
||||||
|
rm -rf $TMPDIR
|
||||||
|
# We are running tests now
|
||||||
|
if docker exec -e SKIP=rum-src,timescaledb-src,rdkit-src,postgis-src,pgx_ulid-src,pgtap-src,pg_tiktoken-src,pg_jsonschema-src,pg_graphql-src,kq_imcx-src,wal2json_2_5-src \
|
||||||
|
$TEST_CONTAINER_NAME /run-tests.sh | tee testout.txt
|
||||||
|
then
|
||||||
|
cleanup
|
||||||
|
else
|
||||||
|
FAILED=$(tail -1 testout.txt)
|
||||||
|
for d in $FAILED
|
||||||
|
do
|
||||||
|
mkdir $d
|
||||||
|
docker cp $TEST_CONTAINER_NAME:/ext-src/$d/regression.diffs $d || true
|
||||||
|
docker cp $TEST_CONTAINER_NAME:/ext-src/$d/regression.out $d || true
|
||||||
|
cat $d/regression.out $d/regression.diffs || true
|
||||||
|
done
|
||||||
|
rm -rf $FAILED
|
||||||
|
cleanup
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
cleanup
|
||||||
done
|
done
|
||||||
|
|||||||
15
docker-compose/run-tests.sh
Normal file
15
docker-compose/run-tests.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -x
|
||||||
|
|
||||||
|
cd /ext-src
|
||||||
|
FAILED=
|
||||||
|
LIST=$((echo ${SKIP} | sed 's/,/\n/g'; ls -d *-src) | sort | uniq -u)
|
||||||
|
for d in ${LIST}
|
||||||
|
do
|
||||||
|
[ -d ${d} ] || continue
|
||||||
|
psql -c "select 1" >/dev/null || break
|
||||||
|
make -C ${d} installcheck || FAILED="${d} ${FAILED}"
|
||||||
|
done
|
||||||
|
[ -z "${FAILED}" ] && exit 0
|
||||||
|
echo ${FAILED}
|
||||||
|
exit 1
|
||||||
@@ -11,15 +11,28 @@ page server. We currently use the same binary for both, with --wal-redo runtime
|
|||||||
the WAL redo mode. Some PostgreSQL changes are needed in the compute node, while others are just for
|
the WAL redo mode. Some PostgreSQL changes are needed in the compute node, while others are just for
|
||||||
the WAL redo process.
|
the WAL redo process.
|
||||||
|
|
||||||
In addition to core PostgreSQL changes, there is a Neon extension in contrib/neon, to hook into the
|
In addition to core PostgreSQL changes, there is a Neon extension in the pgxn/neon directory that
|
||||||
smgr interface. Once all the core changes have been submitted to upstream or eliminated some other
|
hooks into the smgr interface, and rmgr extension in pgxn/neon_rmgr. The extensions are loaded into
|
||||||
way, the extension could live outside the postgres repository and build against vanilla PostgreSQL.
|
the Postgres processes with shared_preload_libraries. Most of the Neon-specific code is in the
|
||||||
|
extensions, and for any new features, that is preferred over modifying core PostgreSQL code.
|
||||||
|
|
||||||
Below is a list of all the PostgreSQL source code changes, categorized into changes needed for
|
Below is a list of all the PostgreSQL source code changes, categorized into changes needed for
|
||||||
compute, and changes needed for the WAL redo process:
|
compute, and changes needed for the WAL redo process:
|
||||||
|
|
||||||
# Changes for Compute node
|
# Changes for Compute node
|
||||||
|
|
||||||
|
## Prefetching
|
||||||
|
|
||||||
|
There are changes in many places to perform prefetching, for example for sequential scans. Neon
|
||||||
|
doesn't benefit from OS readahead, and the latency to pageservers is quite high compared to local
|
||||||
|
disk, so prefetching is critical for performance, also for sequential scans.
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Upcoming "streaming read" work in v17 might simplify this. And async I/O work in v18 will hopefully
|
||||||
|
do more.
|
||||||
|
|
||||||
|
|
||||||
## Add t_cid to heap WAL records
|
## Add t_cid to heap WAL records
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -37,54 +50,11 @@ The problem is that the XLOG_HEAP_INSERT record does not include the command id
|
|||||||
|
|
||||||
Bite the bullet and submit the patch to PostgreSQL, to add the t_cid to the WAL records. It makes the WAL records larger, which could make this unpopular in the PostgreSQL community. However, it might simplify some logical decoding code; Andres Freund briefly mentioned in PGCon 2022 discussion on Heikki's Neon presentation that logical decoding currently needs to jump through some hoops to reconstruct the same information.
|
Bite the bullet and submit the patch to PostgreSQL, to add the t_cid to the WAL records. It makes the WAL records larger, which could make this unpopular in the PostgreSQL community. However, it might simplify some logical decoding code; Andres Freund briefly mentioned in PGCon 2022 discussion on Heikki's Neon presentation that logical decoding currently needs to jump through some hoops to reconstruct the same information.
|
||||||
|
|
||||||
|
Update from Heikki (2024-04-17): I tried to write an upstream patch for that, to use the t_cid field for logical decoding, but it was not as straightforward as it first sounded.
|
||||||
|
|
||||||
### Alternatives
|
### Alternatives
|
||||||
Perhaps we could write an extra WAL record with the t_cid information, when a page is evicted that contains rows that were touched a transaction that's still running. However, that seems very complicated.
|
Perhaps we could write an extra WAL record with the t_cid information, when a page is evicted that contains rows that were touched a transaction that's still running. However, that seems very complicated.
|
||||||
|
|
||||||
## ginfast.c
|
|
||||||
|
|
||||||
```
|
|
||||||
diff --git a/src/backend/access/gin/ginfast.c b/src/backend/access/gin/ginfast.c
|
|
||||||
index e0d9940946..2d964c02e9 100644
|
|
||||||
--- a/src/backend/access/gin/ginfast.c
|
|
||||||
+++ b/src/backend/access/gin/ginfast.c
|
|
||||||
@@ -285,6 +285,17 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector)
|
|
||||||
memset(&sublist, 0, sizeof(GinMetaPageData));
|
|
||||||
makeSublist(index, collector->tuples, collector->ntuples, &sublist);
|
|
||||||
|
|
||||||
+ if (metadata->head != InvalidBlockNumber)
|
|
||||||
+ {
|
|
||||||
+ /*
|
|
||||||
+ * ZENITH: Get buffer before XLogBeginInsert() to avoid recursive call
|
|
||||||
+ * of XLogBeginInsert(). Reading a new buffer might evict a dirty page from
|
|
||||||
+ * the buffer cache, and if that page happens to be an FSM or VM page, zenith_write()
|
|
||||||
+ * will try to WAL-log an image of the page.
|
|
||||||
+ */
|
|
||||||
+ buffer = ReadBuffer(index, metadata->tail);
|
|
||||||
+ }
|
|
||||||
+
|
|
||||||
if (needWal)
|
|
||||||
XLogBeginInsert();
|
|
||||||
|
|
||||||
@@ -316,7 +327,6 @@ ginHeapTupleFastInsert(GinState *ginstate, GinTupleCollector *collector)
|
|
||||||
data.prevTail = metadata->tail;
|
|
||||||
data.newRightlink = sublist.head;
|
|
||||||
|
|
||||||
- buffer = ReadBuffer(index, metadata->tail);
|
|
||||||
LockBuffer(buffer, GIN_EXCLUSIVE);
|
|
||||||
page = BufferGetPage(buffer);
|
|
||||||
```
|
|
||||||
|
|
||||||
The problem is explained in the comment above
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Can we stop WAL-logging FSM or VM pages? Or delay the WAL logging until we're out of the critical
|
|
||||||
section or something.
|
|
||||||
|
|
||||||
Maybe some bigger rewrite of FSM and VM would help to avoid WAL-logging FSM and VM page images?
|
|
||||||
|
|
||||||
|
|
||||||
## Mark index builds that use buffer manager without logging explicitly
|
## Mark index builds that use buffer manager without logging explicitly
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -95,6 +65,8 @@ Maybe some bigger rewrite of FSM and VM would help to avoid WAL-logging FSM and
|
|||||||
also some changes in src/backend/storage/smgr/smgr.c
|
also some changes in src/backend/storage/smgr/smgr.c
|
||||||
```
|
```
|
||||||
|
|
||||||
|
pgvector 0.6.0 also needs a similar change, which would be very nice to get rid of too.
|
||||||
|
|
||||||
When a GIN index is built, for example, it is built by inserting the entries into the index more or
|
When a GIN index is built, for example, it is built by inserting the entries into the index more or
|
||||||
less normally, but without WAL-logging anything. After the index has been built, we iterate through
|
less normally, but without WAL-logging anything. After the index has been built, we iterate through
|
||||||
all pages and write them to the WAL. That doesn't work for Neon, because if a page is not WAL-logged
|
all pages and write them to the WAL. That doesn't work for Neon, because if a page is not WAL-logged
|
||||||
@@ -109,6 +81,10 @@ an operation: `smgr_start_unlogged_build`, `smgr_finish_unlogged_build_phase_1`
|
|||||||
I think it would make sense to be more explicit about that in PostgreSQL too. So extract these
|
I think it would make sense to be more explicit about that in PostgreSQL too. So extract these
|
||||||
changes to a patch and post to pgsql-hackers.
|
changes to a patch and post to pgsql-hackers.
|
||||||
|
|
||||||
|
Perhaps we could deduce that an unlogged index build has started when we see a page being evicted
|
||||||
|
with zero LSN. How to be sure it's an unlogged index build rather than a bug? Currently we have a
|
||||||
|
check for that and PANIC if we see page with zero LSN being evicted. And how do we detect when the
|
||||||
|
index build has finished? See https://github.com/neondatabase/neon/pull/7440 for an attempt at that.
|
||||||
|
|
||||||
## Track last-written page LSN
|
## Track last-written page LSN
|
||||||
|
|
||||||
@@ -140,57 +116,6 @@ The old method is still available, though.
|
|||||||
Wait until v15?
|
Wait until v15?
|
||||||
|
|
||||||
|
|
||||||
## Cache relation sizes
|
|
||||||
|
|
||||||
The Neon extension contains a little cache for smgrnblocks() and smgrexists() calls, to avoid going
|
|
||||||
to the page server every time. It might be useful to cache those in PostgreSQL, maybe in the
|
|
||||||
relcache? (I think we do cache nblocks in relcache already, check why that's not good enough for
|
|
||||||
Neon)
|
|
||||||
|
|
||||||
|
|
||||||
## Use buffer manager when extending VM or FSM
|
|
||||||
|
|
||||||
```
|
|
||||||
src/backend/storage/freespace/freespace.c | 14 +-
|
|
||||||
src/backend/access/heap/visibilitymap.c | 15 +-
|
|
||||||
|
|
||||||
diff --git a/src/backend/access/heap/visibilitymap.c b/src/backend/access/heap/visibilitymap.c
|
|
||||||
index e198df65d8..addfe93eac 100644
|
|
||||||
--- a/src/backend/access/heap/visibilitymap.c
|
|
||||||
+++ b/src/backend/access/heap/visibilitymap.c
|
|
||||||
@@ -652,10 +652,19 @@ vm_extend(Relation rel, BlockNumber vm_nblocks)
|
|
||||||
/* Now extend the file */
|
|
||||||
while (vm_nblocks_now < vm_nblocks)
|
|
||||||
{
|
|
||||||
- PageSetChecksumInplace((Page) pg.data, vm_nblocks_now);
|
|
||||||
+ /*
|
|
||||||
+ * ZENITH: Initialize VM pages through buffer cache to prevent loading
|
|
||||||
+ * them from pageserver.
|
|
||||||
+ */
|
|
||||||
+ Buffer buffer = ReadBufferExtended(rel, VISIBILITYMAP_FORKNUM, P_NEW,
|
|
||||||
+ RBM_ZERO_AND_LOCK, NULL);
|
|
||||||
+ Page page = BufferGetPage(buffer);
|
|
||||||
+
|
|
||||||
+ PageInit((Page) page, BLCKSZ, 0);
|
|
||||||
+ PageSetChecksumInplace(page, vm_nblocks_now);
|
|
||||||
+ MarkBufferDirty(buffer);
|
|
||||||
+ UnlockReleaseBuffer(buffer);
|
|
||||||
|
|
||||||
- smgrextend(rel->rd_smgr, VISIBILITYMAP_FORKNUM, vm_nblocks_now,
|
|
||||||
- pg.data, false);
|
|
||||||
vm_nblocks_now++;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Problem we're trying to solve
|
|
||||||
|
|
||||||
???
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Maybe this would be a reasonable change in PostgreSQL too?
|
|
||||||
|
|
||||||
|
|
||||||
## Allow startup without reading checkpoint record
|
## Allow startup without reading checkpoint record
|
||||||
|
|
||||||
In Neon, the compute node is stateless. So when we are launching compute node, we need to provide
|
In Neon, the compute node is stateless. So when we are launching compute node, we need to provide
|
||||||
@@ -231,7 +156,7 @@ index 0415df9ccb..9f9db3c8bc 100644
|
|||||||
* crash we can lose (skip over) as many values as we pre-logged.
|
* crash we can lose (skip over) as many values as we pre-logged.
|
||||||
*/
|
*/
|
||||||
-#define SEQ_LOG_VALS 32
|
-#define SEQ_LOG_VALS 32
|
||||||
+/* Zenith XXX: to ensure sequence order of sequence in Zenith we need to WAL log each sequence update. */
|
+/* Neon XXX: to ensure sequence order of sequence in Zenith we need to WAL log each sequence update. */
|
||||||
+/* #define SEQ_LOG_VALS 32 */
|
+/* #define SEQ_LOG_VALS 32 */
|
||||||
+#define SEQ_LOG_VALS 0
|
+#define SEQ_LOG_VALS 0
|
||||||
```
|
```
|
||||||
@@ -250,66 +175,6 @@ would be weird if the sequence moved backwards though, think of PITR.
|
|||||||
Or add a GUC for the amount to prefix to PostgreSQL, and force it to 1 in Neon.
|
Or add a GUC for the amount to prefix to PostgreSQL, and force it to 1 in Neon.
|
||||||
|
|
||||||
|
|
||||||
## Walproposer
|
|
||||||
|
|
||||||
```
|
|
||||||
src/Makefile | 1 +
|
|
||||||
src/backend/replication/libpqwalproposer/Makefile | 37 +
|
|
||||||
src/backend/replication/libpqwalproposer/libpqwalproposer.c | 416 ++++++++++++
|
|
||||||
src/backend/postmaster/bgworker.c | 4 +
|
|
||||||
src/backend/postmaster/postmaster.c | 6 +
|
|
||||||
src/backend/replication/Makefile | 4 +-
|
|
||||||
src/backend/replication/walproposer.c | 2350 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
src/backend/replication/walproposer_utils.c | 402 +++++++++++
|
|
||||||
src/backend/replication/walreceiver.c | 7 +
|
|
||||||
src/backend/replication/walsender.c | 320 ++++++---
|
|
||||||
src/backend/storage/ipc/ipci.c | 6 +
|
|
||||||
src/include/replication/walproposer.h | 565 ++++++++++++++++
|
|
||||||
```
|
|
||||||
|
|
||||||
WAL proposer is communicating with safekeeper and ensures WAL durability by quorum writes. It is
|
|
||||||
currently implemented as patch to standard WAL sender.
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Refactor into an extension. Submit hooks or APIs into upstream if necessary.
|
|
||||||
|
|
||||||
@MMeent did some work on this already: https://github.com/neondatabase/postgres/pull/96
|
|
||||||
|
|
||||||
## Ignore unexpected data beyond EOF in bufmgr.c
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -922,11 +928,14 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
|
|
||||||
*/
|
|
||||||
bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr);
|
|
||||||
if (!PageIsNew((Page) bufBlock))
|
|
||||||
- ereport(ERROR,
|
|
||||||
+ {
|
|
||||||
+ // XXX-ZENITH
|
|
||||||
+ MemSet((char *) bufBlock, 0, BLCKSZ);
|
|
||||||
+ ereport(DEBUG1,
|
|
||||||
(errmsg("unexpected data beyond EOF in block %u of relation %s",
|
|
||||||
blockNum, relpath(smgr->smgr_rnode, forkNum)),
|
|
||||||
errhint("This has been seen to occur with buggy kernels; consider updating your system.")));
|
|
||||||
-
|
|
||||||
+ }
|
|
||||||
/*
|
|
||||||
* We *must* do smgrextend before succeeding, else the page will not
|
|
||||||
* be reserved by the kernel, and the next P_NEW call will decide to
|
|
||||||
```
|
|
||||||
|
|
||||||
PostgreSQL is a bit sloppy with extending relations. Usually, the relation is extended with zeros
|
|
||||||
first, then the page is filled, and finally the new page WAL-logged. But if multiple backends extend
|
|
||||||
a relation at the same time, the pages can be WAL-logged in different order.
|
|
||||||
|
|
||||||
I'm not sure what scenario exactly required this change in Neon, though.
|
|
||||||
|
|
||||||
### How to get rid of the patch
|
|
||||||
|
|
||||||
Submit patches to pgsql-hackers, to tighten up the WAL-logging around relation extension. It's a bit
|
|
||||||
confusing even in PostgreSQL. Maybe WAL log the intention to extend first, then extend the relation,
|
|
||||||
and finally WAL-log that the extension succeeded.
|
|
||||||
|
|
||||||
## Make smgr interface available to extensions
|
## Make smgr interface available to extensions
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -321,6 +186,8 @@ and finally WAL-log that the extension succeeded.
|
|||||||
|
|
||||||
Submit to upstream. This could be useful for the Disk Encryption patches too, or for compression.
|
Submit to upstream. This could be useful for the Disk Encryption patches too, or for compression.
|
||||||
|
|
||||||
|
We have submitted this to upstream, but it's moving at glacial a speed.
|
||||||
|
https://commitfest.postgresql.org/47/4428/
|
||||||
|
|
||||||
## Added relpersistence argument to smgropen()
|
## Added relpersistence argument to smgropen()
|
||||||
|
|
||||||
@@ -444,6 +311,148 @@ Ignore it. This is only needed for disaster recovery, so once we've eliminated a
|
|||||||
patches, we can just keep it around as a patch or as separate branch in a repo.
|
patches, we can just keep it around as a patch or as separate branch in a repo.
|
||||||
|
|
||||||
|
|
||||||
|
## pg_waldump flags to ignore errors
|
||||||
|
|
||||||
|
After creating a new project or branch in Neon, the first timeline can begin in the middle of a WAL segment. pg_waldump chokes on that, so we added some flags to make it possible to ignore errors.
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Like previous one, ignore it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Backpressure if pageserver doesn't ingest WAL fast enough
|
||||||
|
|
||||||
|
```
|
||||||
|
@@ -3200,6 +3202,7 @@ ProcessInterrupts(void)
|
||||||
|
return;
|
||||||
|
InterruptPending = false;
|
||||||
|
|
||||||
|
+retry:
|
||||||
|
if (ProcDiePending)
|
||||||
|
{
|
||||||
|
ProcDiePending = false;
|
||||||
|
@@ -3447,6 +3450,13 @@ ProcessInterrupts(void)
|
||||||
|
|
||||||
|
if (ParallelApplyMessagePending)
|
||||||
|
HandleParallelApplyMessages();
|
||||||
|
+
|
||||||
|
+ /* Call registered callback if any */
|
||||||
|
+ if (ProcessInterruptsCallback)
|
||||||
|
+ {
|
||||||
|
+ if (ProcessInterruptsCallback())
|
||||||
|
+ goto retry;
|
||||||
|
+ }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Submit a patch to upstream, for a hook in ProcessInterrupts. Could be useful for other extensions
|
||||||
|
too.
|
||||||
|
|
||||||
|
|
||||||
|
## SLRU on-demand download
|
||||||
|
|
||||||
|
```
|
||||||
|
src/backend/access/transam/slru.c | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
|
||||||
|
1 file changed, 92 insertions(+), 13 deletions(-)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Problem we're trying to solve
|
||||||
|
|
||||||
|
Previously, SLRU files were included in the basebackup, but the total size of them can be large,
|
||||||
|
several GB, and downloading them all made the startup time too long.
|
||||||
|
|
||||||
|
### Alternatives
|
||||||
|
|
||||||
|
FUSE hook or LD_PRELOAD trick to intercept the reads on SLRU files
|
||||||
|
|
||||||
|
|
||||||
|
## WAL-log an all-zeros page as one large hole
|
||||||
|
|
||||||
|
- In XLogRecordAssemble()
|
||||||
|
|
||||||
|
### Problem we're trying to solve
|
||||||
|
|
||||||
|
This change was made in v16. Starting with v16, when PostgreSQL extends a relation, it first extends
|
||||||
|
it with zeros, and it can extend the relation more than one block at a time. The all-zeros page is WAL-ogged, but it's very wasteful to include 8 kB of zeros in the WAL for that. This hack was made so that we WAL logged a compact record with a whole-page "hole". However, PostgreSQL has assertions that prevent that such WAL records from being replayed, so this breaks compatibility such that unmodified PostreSQL cannot process Neon-generated WAL.
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Find another compact representation for a full-page image of an all-zeros page. A compressed image perhaps.
|
||||||
|
|
||||||
|
|
||||||
|
## Shut down walproposer after checkpointer
|
||||||
|
|
||||||
|
```
|
||||||
|
+ /* Neon: Also allow walproposer background worker to be treated like a WAL sender, so that it's shut down last */
|
||||||
|
+ if ((bp->bkend_type == BACKEND_TYPE_NORMAL || bp->bkend_type == BACKEND_TYPE_BGWORKER) &&
|
||||||
|
```
|
||||||
|
|
||||||
|
This changes was needed so that postmaster shuts down the walproposer process only after the shutdown checkpoint record is written. Otherwise, the shutdown record will never make it to the safekeepers.
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Do a bigger refactoring of the postmaster state machine, such that a background worker can specify
|
||||||
|
the shutdown ordering by itself. The postmaster state machine has grown pretty complicated, and
|
||||||
|
would benefit from a refactoring for the sake of readability anyway.
|
||||||
|
|
||||||
|
|
||||||
|
## EXPLAIN changes for prefetch and LFC
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Konstantin submitted a patch to -hackers already: https://commitfest.postgresql.org/47/4643/. Get that into a committable state.
|
||||||
|
|
||||||
|
|
||||||
|
## On-demand download of extensions
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
FUSE or LD_PRELOAD trickery to intercept reads?
|
||||||
|
|
||||||
|
|
||||||
|
## Publication superuser checks
|
||||||
|
|
||||||
|
We have hacked CreatePublication so that also neon_superuser can create them.
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Create an upstream patch with more fine-grained privileges for publications CREATE/DROP that can be GRANTed to users.
|
||||||
|
|
||||||
|
|
||||||
|
## WAL log replication slots
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
Utilize the upcoming v17 "slot sync worker", or a similar neon-specific background worker process, to periodically WAL-log the slots, or to export them somewhere else.
|
||||||
|
|
||||||
|
|
||||||
|
## WAL-log replication snapshots
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
WAL-log them periodically, from a backgound worker.
|
||||||
|
|
||||||
|
|
||||||
|
## WAL-log relmapper files
|
||||||
|
|
||||||
|
Similarly to replications snapshot files, the CID mapping files generated during VACUUM FULL of a catalog table are WAL-logged
|
||||||
|
|
||||||
|
### How to get rid of the patch
|
||||||
|
|
||||||
|
WAL-log them periodically, from a backgound worker.
|
||||||
|
|
||||||
|
|
||||||
|
## XLogWaitForReplayOf()
|
||||||
|
|
||||||
|
??
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Not currently committed but proposed
|
# Not currently committed but proposed
|
||||||
|
|
||||||
## Disable ring buffer buffer manager strategies
|
## Disable ring buffer buffer manager strategies
|
||||||
@@ -472,23 +481,10 @@ hint bits are set. Wal logging hint bits updates requires FPI which significantl
|
|||||||
|
|
||||||
Add special WAL record for setting page hints.
|
Add special WAL record for setting page hints.
|
||||||
|
|
||||||
## Prefetching
|
|
||||||
|
|
||||||
### Why?
|
|
||||||
|
|
||||||
As far as pages in Neon are loaded on demand, to reduce node startup time
|
|
||||||
and also speedup some massive queries we need some mechanism for bulk loading to
|
|
||||||
reduce page request round-trip overhead.
|
|
||||||
|
|
||||||
Currently Postgres is supporting prefetching only for bitmap scan.
|
|
||||||
In Neon we should also use prefetch for sequential and index scans, because the OS is not doing it for us.
|
|
||||||
For sequential scan we could prefetch some number of following pages. For index scan we could prefetch pages
|
|
||||||
of heap relation addressed by TIDs.
|
|
||||||
|
|
||||||
## Prewarming
|
## Prewarming
|
||||||
|
|
||||||
### Why?
|
### Why?
|
||||||
|
|
||||||
Short downtime (or, in other words, fast compute node restart time) is one of the key feature of Zenith.
|
Short downtime (or, in other words, fast compute node restart time) is one of the key feature of Neon.
|
||||||
But overhead of request-response round-trip for loading pages on demand can make started node warm-up quite slow.
|
But overhead of request-response round-trip for loading pages on demand can make started node warm-up quite slow.
|
||||||
We can capture state of compute node buffer cache and send bulk request for this pages at startup.
|
We can capture state of compute node buffer cache and send bulk request for this pages at startup.
|
||||||
|
|||||||
@@ -4,18 +4,18 @@
|
|||||||
|
|
||||||
Currently we build two main images:
|
Currently we build two main images:
|
||||||
|
|
||||||
- [neondatabase/neon](https://hub.docker.com/repository/docker/zenithdb/zenith) — image with pre-built `pageserver`, `safekeeper` and `proxy` binaries and all the required runtime dependencies. Built from [/Dockerfile](/Dockerfile).
|
- [neondatabase/neon](https://hub.docker.com/repository/docker/neondatabase/neon) — image with pre-built `pageserver`, `safekeeper` and `proxy` binaries and all the required runtime dependencies. Built from [/Dockerfile](/Dockerfile).
|
||||||
- [neondatabase/compute-node](https://hub.docker.com/repository/docker/zenithdb/compute-node) — compute node image with pre-built Postgres binaries from [neondatabase/postgres](https://github.com/neondatabase/postgres).
|
- [neondatabase/compute-node-v16](https://hub.docker.com/repository/docker/neondatabase/compute-node-v16) — compute node image with pre-built Postgres binaries from [neondatabase/postgres](https://github.com/neondatabase/postgres). Similar images exist for v15 and v14.
|
||||||
|
|
||||||
And additional intermediate image:
|
And additional intermediate image:
|
||||||
|
|
||||||
- [neondatabase/compute-tools](https://hub.docker.com/repository/docker/neondatabase/compute-tools) — compute node configuration management tools.
|
- [neondatabase/compute-tools](https://hub.docker.com/repository/docker/neondatabase/compute-tools) — compute node configuration management tools.
|
||||||
|
|
||||||
## Building pipeline
|
## Build pipeline
|
||||||
|
|
||||||
We build all images after a successful `release` tests run and push automatically to Docker Hub with two parallel CI jobs
|
We build all images after a successful `release` tests run and push automatically to Docker Hub with two parallel CI jobs
|
||||||
|
|
||||||
1. `neondatabase/compute-tools` and `neondatabase/compute-node`
|
1. `neondatabase/compute-tools` and `neondatabase/compute-node-v16` (and -v15 and -v14)
|
||||||
|
|
||||||
2. `neondatabase/neon`
|
2. `neondatabase/neon`
|
||||||
|
|
||||||
@@ -34,12 +34,12 @@ You can see a [docker compose](https://docs.docker.com/compose/) example to crea
|
|||||||
1. create containers
|
1. create containers
|
||||||
|
|
||||||
You can specify version of neon cluster using following environment values.
|
You can specify version of neon cluster using following environment values.
|
||||||
- PG_VERSION: postgres version for compute (default is 14)
|
- PG_VERSION: postgres version for compute (default is 16 as of this writing)
|
||||||
- TAG: the tag version of [docker image](https://registry.hub.docker.com/r/neondatabase/neon/tags) (default is latest), which is tagged in [CI test](/.github/workflows/build_and_test.yml)
|
- TAG: the tag version of [docker image](https://registry.hub.docker.com/r/neondatabase/neon/tags), which is tagged in [CI test](/.github/workflows/build_and_test.yml). Default is 'latest'
|
||||||
```
|
```
|
||||||
$ cd docker-compose/
|
$ cd docker-compose/
|
||||||
$ docker-compose down # remove the containers if exists
|
$ docker-compose down # remove the containers if exists
|
||||||
$ PG_VERSION=15 TAG=2937 docker-compose up --build -d # You can specify the postgres and image version
|
$ PG_VERSION=16 TAG=latest docker-compose up --build -d # You can specify the postgres and image version
|
||||||
Creating network "dockercompose_default" with the default driver
|
Creating network "dockercompose_default" with the default driver
|
||||||
Creating docker-compose_storage_broker_1 ... done
|
Creating docker-compose_storage_broker_1 ... done
|
||||||
(...omit...)
|
(...omit...)
|
||||||
@@ -47,29 +47,31 @@ Creating docker-compose_storage_broker_1 ... done
|
|||||||
|
|
||||||
2. connect compute node
|
2. connect compute node
|
||||||
```
|
```
|
||||||
$ echo "localhost:55433:postgres:cloud_admin:cloud_admin" >> ~/.pgpass
|
$ psql postgresql://cloud_admin:cloud_admin@localhost:55433/postgres
|
||||||
$ chmod 600 ~/.pgpass
|
psql (16.3)
|
||||||
$ psql -h localhost -p 55433 -U cloud_admin
|
Type "help" for help.
|
||||||
|
|
||||||
postgres=# CREATE TABLE t(key int primary key, value text);
|
postgres=# CREATE TABLE t(key int primary key, value text);
|
||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
postgres=# insert into t values(1,1);
|
postgres=# insert into t values(1, 1);
|
||||||
INSERT 0 1
|
INSERT 0 1
|
||||||
postgres=# select * from t;
|
postgres=# select * from t;
|
||||||
key | value
|
key | value
|
||||||
-----+-------
|
-----+-------
|
||||||
1 | 1
|
1 | 1
|
||||||
(1 row)
|
(1 row)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. If you want to see the log, you can use `docker-compose logs` command.
|
3. If you want to see the log, you can use `docker-compose logs` command.
|
||||||
```
|
```
|
||||||
# check the container name you want to see
|
# check the container name you want to see
|
||||||
$ docker ps
|
$ docker ps
|
||||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||||
d6968a5ae912 dockercompose_compute "/shell/compute.sh" 5 minutes ago Up 5 minutes 0.0.0.0:3080->3080/tcp, 0.0.0.0:55433->55433/tcp dockercompose_compute_1
|
3582f6d76227 docker-compose_compute "/shell/compute.sh" 2 minutes ago Up 2 minutes 0.0.0.0:3080->3080/tcp, :::3080->3080/tcp, 0.0.0.0:55433->55433/tcp, :::55433->55433/tcp docker-compose_compute_1
|
||||||
(...omit...)
|
(...omit...)
|
||||||
|
|
||||||
$ docker logs -f dockercompose_compute_1
|
$ docker logs -f docker-compose_compute_1
|
||||||
2022-10-21 06:15:48.757 GMT [56] LOG: connection authorized: user=cloud_admin database=postgres application_name=psql
|
2022-10-21 06:15:48.757 GMT [56] LOG: connection authorized: user=cloud_admin database=postgres application_name=psql
|
||||||
2022-10-21 06:17:00.307 GMT [56] LOG: [NEON_SMGR] libpagestore: connected to 'host=pageserver port=6400'
|
2022-10-21 06:17:00.307 GMT [56] LOG: [NEON_SMGR] libpagestore: connected to 'host=pageserver port=6400'
|
||||||
(...omit...)
|
(...omit...)
|
||||||
|
|||||||
@@ -5,4 +5,3 @@ TODO:
|
|||||||
- shared across tenants
|
- shared across tenants
|
||||||
- store pages from layer files
|
- store pages from layer files
|
||||||
- store pages from "in-memory layer"
|
- store pages from "in-memory layer"
|
||||||
- store materialized pages
|
|
||||||
|
|||||||
@@ -101,11 +101,12 @@ or
|
|||||||
```toml
|
```toml
|
||||||
[remote_storage]
|
[remote_storage]
|
||||||
container_name = 'some-container-name'
|
container_name = 'some-container-name'
|
||||||
|
storage_account = 'somestorageaccnt'
|
||||||
container_region = 'us-east'
|
container_region = 'us-east'
|
||||||
prefix_in_container = '/test-prefix/'
|
prefix_in_container = '/test-prefix/'
|
||||||
```
|
```
|
||||||
|
|
||||||
`AZURE_STORAGE_ACCOUNT` and `AZURE_STORAGE_ACCESS_KEY` env variables can be used to specify the azure credentials if needed.
|
The `AZURE_STORAGE_ACCESS_KEY` env variable can be used to specify the azure credentials if needed.
|
||||||
|
|
||||||
## Repository background tasks
|
## Repository background tasks
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ depends on that, so if you change it, bad things will happen.
|
|||||||
|
|
||||||
#### page_cache_size
|
#### page_cache_size
|
||||||
|
|
||||||
Size of the page cache, to hold materialized page versions. Unit is
|
Size of the page cache. Unit is
|
||||||
number of 8 kB blocks. The default is 8192, which means 64 MB.
|
number of 8 kB blocks. The default is 8192, which means 64 MB.
|
||||||
|
|
||||||
#### max_file_descriptors
|
#### max_file_descriptors
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ Below you will find a brief overview of each subdir in the source tree in alphab
|
|||||||
Neon storage broker, providing messaging between safekeepers and pageservers.
|
Neon storage broker, providing messaging between safekeepers and pageservers.
|
||||||
[storage_broker.md](./storage_broker.md)
|
[storage_broker.md](./storage_broker.md)
|
||||||
|
|
||||||
|
`storage_controller`:
|
||||||
|
|
||||||
|
Neon storage controller, manages a cluster of pageservers and exposes an API that enables
|
||||||
|
managing a many-sharded tenant as a single entity.
|
||||||
|
|
||||||
`/control_plane`:
|
`/control_plane`:
|
||||||
|
|
||||||
Local control plane.
|
Local control plane.
|
||||||
|
|||||||
150
docs/storage_controller.md
Normal file
150
docs/storage_controller.md
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
# Storage Controller
|
||||||
|
|
||||||
|
## Concepts
|
||||||
|
|
||||||
|
The storage controller sits between administrative API clients and pageservers, and handles the details of mapping tenants to pageserver tenant shards. For example, creating a tenant is one API call to the storage controller,
|
||||||
|
which is mapped into many API calls to many pageservers (for multiple shards, and for secondary locations).
|
||||||
|
|
||||||
|
It implements a pageserver-compatible API that may be used for CRUD operations on tenants and timelines, translating these requests into appropriate operations on the shards within a tenant, which may be on many different pageservers. Using this API, the storage controller may be used in the same way as the pageserver's administrative HTTP API, hiding
|
||||||
|
the underlying details of how data is spread across multiple nodes.
|
||||||
|
|
||||||
|
The storage controller also manages generations, high availability (via secondary locations) and live migrations for tenants under its management. This is done with a reconciliation loop pattern, where tenants have an “intent” state and a “reconcile” task that tries to make the outside world match the intent.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
The storage controller’s HTTP server implements four logically separate APIs:
|
||||||
|
|
||||||
|
- `/v1/...` path is the pageserver-compatible API. This has to be at the path root because that’s where clients expect to find it on a pageserver.
|
||||||
|
- `/control/v1/...` path is the storage controller’s API, which enables operations such as registering and management pageservers, or executing shard splits.
|
||||||
|
- `/debug/v1/...` path contains endpoints which are either exclusively used in tests, or are for use by engineers when supporting a deployed system.
|
||||||
|
- `/upcall/v1/...` path contains endpoints that are called by pageservers. This includes the `/re-attach` and `/validate` APIs used by pageservers
|
||||||
|
to ensure data safety with generation numbers.
|
||||||
|
|
||||||
|
The API is authenticated with a JWT token, and tokens must have scope `pageserverapi` (i.e. the same scope as pageservers’ APIs).
|
||||||
|
|
||||||
|
See the `http.rs` file in the source for where the HTTP APIs are implemented.
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
The storage controller uses a postgres database to persist a subset of its state. Note that the storage controller does _not_ keep all its state in the database: this is a design choice to enable most operations to be done efficiently in memory, rather than having to read from the database. See `persistence.rs` for a more comprehensive comment explaining what we do and do not persist: a useful metaphor is that we persist objects like tenants and nodes, but we do not
|
||||||
|
persist the _relationships_ between them: the attachment state of a tenant's shards to nodes is kept in memory and
|
||||||
|
rebuilt on startup.
|
||||||
|
|
||||||
|
The file `persistence.rs` contains all the code for accessing the database, and has a large doc comment that goes into more detail about exactly what we persist and why.
|
||||||
|
|
||||||
|
The `diesel` crate is used for defining models & migrations.
|
||||||
|
|
||||||
|
Running a local cluster with `cargo neon` automatically starts a vanilla postgress process to host the storage controller’s database.
|
||||||
|
|
||||||
|
### Diesel tip: migrations
|
||||||
|
|
||||||
|
If you need to modify the database schema, here’s how to create a migration:
|
||||||
|
|
||||||
|
- Install the diesel CLI with `cargo install diesel_cli`
|
||||||
|
- Use `diesel migration generate <name>` to create a new migration
|
||||||
|
- Populate the SQL files in the `migrations/` subdirectory
|
||||||
|
- Use `DATABASE_URL=... diesel migration run` to apply the migration you just wrote: this will update the `[schema.rs](http://schema.rs)` file automatically.
|
||||||
|
- This requires a running database: the easiest way to do that is to just run `cargo neon init ; cargo neon start`, which will leave a database available at `postgresql://localhost:1235/attachment_service`
|
||||||
|
- Commit the migration files and the changes to schema.rs
|
||||||
|
- If you need to iterate, you can rewind migrations with `diesel migration revert -a` and then `diesel migration run` again.
|
||||||
|
- The migrations are build into the storage controller binary, and automatically run at startup after it is deployed, so once you’ve committed a migration no further steps are needed.
|
||||||
|
|
||||||
|
## storcon_cli
|
||||||
|
|
||||||
|
The `storcon_cli` tool enables interactive management of the storage controller. This is usually
|
||||||
|
only necessary for debug, but may also be used to manage nodes (e.g. marking a node as offline).
|
||||||
|
|
||||||
|
`storcon_cli --help` includes details on commands.
|
||||||
|
|
||||||
|
# Deploying
|
||||||
|
|
||||||
|
This section is aimed at engineers deploying the storage controller outside of Neon's cloud platform, as
|
||||||
|
part of a self-hosted system.
|
||||||
|
|
||||||
|
_General note: since the default `neon_local` environment includes a storage controller, this is a useful
|
||||||
|
reference when figuring out deployment._
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
It is **essential** that the database used by the storage controller is durable (**do not store it on ephemeral
|
||||||
|
local disk**). This database contains pageserver generation numbers, which are essential to data safety on the pageserver.
|
||||||
|
|
||||||
|
The resource requirements for the database are very low: a single CPU core and 1GiB of memory should work well for most deployments. The physical size of the database is typically under a gigabyte.
|
||||||
|
|
||||||
|
Set the URL to the database using the `--database-url` CLI option.
|
||||||
|
|
||||||
|
There is no need to run migrations manually: the storage controller automatically applies migrations
|
||||||
|
when it starts up.
|
||||||
|
|
||||||
|
## Configure pageservers to use the storage controller
|
||||||
|
|
||||||
|
1. The pageserver `control_plane_api` and `control_plane_api_token` should be set in the `pageserver.toml` file. The API setting should
|
||||||
|
point to the "upcall" prefix, for example `http://127.0.0.1:1234/upcall/v1/` is used in neon_local clusters.
|
||||||
|
2. Create a `metadata.json` file in the same directory as `pageserver.toml`: this enables the pageserver to automatically register itself
|
||||||
|
with the storage controller when it starts up. See the example below for the format of this file.
|
||||||
|
|
||||||
|
### Example `metadata.json`
|
||||||
|
|
||||||
|
```
|
||||||
|
{"host":"acmehost.localdomain","http_host":"acmehost.localdomain","http_port":9898,"port":64000}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `port` and `host` refer to the _postgres_ port and host, and these must be accessible from wherever
|
||||||
|
postgres runs.
|
||||||
|
- `http_port` and `http_host` refer to the pageserver's HTTP api, this must be accessible from where
|
||||||
|
the storage controller runs.
|
||||||
|
|
||||||
|
## Handle compute notifications.
|
||||||
|
|
||||||
|
The storage controller independently moves tenant attachments between pageservers in response to
|
||||||
|
changes such as a pageserver node becoming unavailable, or the tenant's shard count changing. To enable
|
||||||
|
postgres clients to handle such changes, the storage controller calls an API hook when a tenant's pageserver
|
||||||
|
location changes.
|
||||||
|
|
||||||
|
The hook is configured using the storage controller's `--compute-hook-url` CLI option. If the hook requires
|
||||||
|
JWT auth, the token may be provided with `--control-plane-jwt-token`. The hook will be invoked with a `PUT` request.
|
||||||
|
|
||||||
|
In the Neon cloud service, this hook is implemented by Neon's internal cloud control plane. In `neon_local` systems
|
||||||
|
the storage controller integrates directly with neon_local to reconfigure local postgres processes instead of calling
|
||||||
|
the compute hook.
|
||||||
|
|
||||||
|
When implementing an on-premise Neon deployment, you must implement a service that handles the compute hook. This is not complicated:
|
||||||
|
the request body has format of the `ComputeHookNotifyRequest` structure, provided below for convenience.
|
||||||
|
|
||||||
|
```
|
||||||
|
struct ComputeHookNotifyRequestShard {
|
||||||
|
node_id: NodeId,
|
||||||
|
shard_number: ShardNumber,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ComputeHookNotifyRequest {
|
||||||
|
tenant_id: TenantId,
|
||||||
|
stripe_size: Option<ShardStripeSize>,
|
||||||
|
shards: Vec<ComputeHookNotifyRequestShard>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
When a notification is received:
|
||||||
|
|
||||||
|
1. Modify postgres configuration for this tenant:
|
||||||
|
|
||||||
|
- set `neon.pageserver_connstr` to a comma-separated list of postgres connection strings to pageservers according to the `shards` list. The
|
||||||
|
shards identified by `NodeId` must be converted to the address+port of the node.
|
||||||
|
- if stripe_size is not None, set `neon.stripe_size` to this value
|
||||||
|
|
||||||
|
2. Send SIGHUP to postgres to reload configuration
|
||||||
|
3. Respond with 200 to the notification request. Do not return success if postgres was not updated: if an error is returned, the controller
|
||||||
|
will retry the notification until it succeeds..
|
||||||
|
|
||||||
|
### Example notification body
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"tenant_id": "1f359dd625e519a1a4e8d7509690f6fc",
|
||||||
|
"stripe_size": 32768,
|
||||||
|
"shards": [
|
||||||
|
{"node_id": 344, "shard_number": 0},
|
||||||
|
{"node_id": 722, "shard_number": 1},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
use crate::spec::ComputeSpec;
|
use crate::spec::{ComputeSpec, Database, Role};
|
||||||
|
|
||||||
#[derive(Serialize, Debug, Deserialize)]
|
#[derive(Serialize, Debug, Deserialize)]
|
||||||
pub struct GenericAPIError {
|
pub struct GenericAPIError {
|
||||||
@@ -113,6 +113,12 @@ pub struct ComputeMetrics {
|
|||||||
pub total_ext_download_size: u64,
|
pub total_ext_download_size: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize)]
|
||||||
|
pub struct CatalogObjects {
|
||||||
|
pub roles: Vec<Role>,
|
||||||
|
pub databases: Vec<Database>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Response of the `/computes/{compute_id}/spec` control-plane API.
|
/// Response of the `/computes/{compute_id}/spec` control-plane API.
|
||||||
/// This is not actually a compute API response, so consider moving
|
/// This is not actually a compute API response, so consider moving
|
||||||
/// to a different place.
|
/// to a different place.
|
||||||
|
|||||||
@@ -33,6 +33,23 @@ pub struct ComputeSpec {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub features: Vec<ComputeFeature>,
|
pub features: Vec<ComputeFeature>,
|
||||||
|
|
||||||
|
/// If compute_ctl was passed `--resize-swap-on-bind`, a value of `Some(_)` instructs
|
||||||
|
/// compute_ctl to `/neonvm/bin/resize-swap` with the given size, when the spec is first
|
||||||
|
/// received.
|
||||||
|
///
|
||||||
|
/// Both this field and `--resize-swap-on-bind` are required, so that the control plane's
|
||||||
|
/// spec generation doesn't need to be aware of the actual compute it's running on, while
|
||||||
|
/// guaranteeing gradual rollout of swap. Otherwise, without `--resize-swap-on-bind`, we could
|
||||||
|
/// end up trying to resize swap in VMs without it -- or end up *not* resizing swap, thus
|
||||||
|
/// giving every VM much more swap than it should have (32GiB).
|
||||||
|
///
|
||||||
|
/// Eventually we may remove `--resize-swap-on-bind` and exclusively use `swap_size_bytes` for
|
||||||
|
/// enabling the swap resizing behavior once rollout is complete.
|
||||||
|
///
|
||||||
|
/// See neondatabase/cloud#12047 for more.
|
||||||
|
#[serde(default)]
|
||||||
|
pub swap_size_bytes: Option<u64>,
|
||||||
|
|
||||||
/// Expected cluster state at the end of transition process.
|
/// Expected cluster state at the end of transition process.
|
||||||
pub cluster: Cluster,
|
pub cluster: Cluster,
|
||||||
pub delta_operations: Option<Vec<DeltaOp>>,
|
pub delta_operations: Option<Vec<DeltaOp>>,
|
||||||
@@ -79,12 +96,6 @@ pub struct ComputeSpec {
|
|||||||
// Stripe size for pageserver sharding, in pages
|
// Stripe size for pageserver sharding, in pages
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub shard_stripe_size: Option<usize>,
|
pub shard_stripe_size: Option<usize>,
|
||||||
|
|
||||||
// When we are starting a new replica in hot standby mode,
|
|
||||||
// we need to know if the primary is running.
|
|
||||||
// This is used to determine if replica should wait for
|
|
||||||
// RUNNING_XACTS from primary or not.
|
|
||||||
pub primary_is_running: Option<bool>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
|
/// Feature flag to signal `compute_ctl` to enable certain experimental functionality.
|
||||||
|
|||||||
@@ -10,11 +10,13 @@ libc.workspace = true
|
|||||||
once_cell.workspace = true
|
once_cell.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
twox-hash.workspace = true
|
twox-hash.workspace = true
|
||||||
|
measured.workspace = true
|
||||||
|
|
||||||
workspace_hack.workspace = true
|
workspace_hack.workspace = true
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
procfs.workspace = true
|
procfs.workspace = true
|
||||||
|
measured-process.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|||||||
@@ -7,14 +7,19 @@
|
|||||||
//! use significantly less memory than this, but can only approximate the cardinality.
|
//! use significantly less memory than this, but can only approximate the cardinality.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
hash::{BuildHasher, BuildHasherDefault, Hash},
|
||||||
hash::{BuildHasher, BuildHasherDefault, Hash, Hasher},
|
sync::atomic::AtomicU8,
|
||||||
sync::{atomic::AtomicU8, Arc, RwLock},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use prometheus::{
|
use measured::{
|
||||||
core::{self, Describer},
|
label::{LabelGroupVisitor, LabelName, LabelValue, LabelVisitor},
|
||||||
proto, Opts,
|
metric::{
|
||||||
|
group::{Encoding, MetricValue},
|
||||||
|
name::MetricNameEncoder,
|
||||||
|
Metric, MetricType, MetricVec,
|
||||||
|
},
|
||||||
|
text::TextEncoder,
|
||||||
|
LabelGroup,
|
||||||
};
|
};
|
||||||
use twox_hash::xxh3;
|
use twox_hash::xxh3;
|
||||||
|
|
||||||
@@ -93,203 +98,25 @@ macro_rules! register_hll {
|
|||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// See <https://en.wikipedia.org/wiki/HyperLogLog#Practical_considerations> for estimates on alpha
|
/// See <https://en.wikipedia.org/wiki/HyperLogLog#Practical_considerations> for estimates on alpha
|
||||||
#[derive(Clone)]
|
pub type HyperLogLogVec<L, const N: usize> = MetricVec<HyperLogLogState<N>, L>;
|
||||||
pub struct HyperLogLogVec<const N: usize> {
|
pub type HyperLogLog<const N: usize> = Metric<HyperLogLogState<N>>;
|
||||||
core: Arc<HyperLogLogVecCore<N>>,
|
|
||||||
|
pub struct HyperLogLogState<const N: usize> {
|
||||||
|
shards: [AtomicU8; N],
|
||||||
}
|
}
|
||||||
|
impl<const N: usize> Default for HyperLogLogState<N> {
|
||||||
struct HyperLogLogVecCore<const N: usize> {
|
fn default() -> Self {
|
||||||
pub children: RwLock<HashMap<u64, HyperLogLog<N>, BuildHasherDefault<xxh3::Hash64>>>,
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
pub desc: core::Desc,
|
const ZERO: AtomicU8 = AtomicU8::new(0);
|
||||||
pub opts: Opts,
|
Self { shards: [ZERO; N] }
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> core::Collector for HyperLogLogVec<N> {
|
|
||||||
fn desc(&self) -> Vec<&core::Desc> {
|
|
||||||
vec![&self.core.desc]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn collect(&self) -> Vec<proto::MetricFamily> {
|
|
||||||
let mut m = proto::MetricFamily::default();
|
|
||||||
m.set_name(self.core.desc.fq_name.clone());
|
|
||||||
m.set_help(self.core.desc.help.clone());
|
|
||||||
m.set_field_type(proto::MetricType::GAUGE);
|
|
||||||
|
|
||||||
let mut metrics = Vec::new();
|
|
||||||
for child in self.core.children.read().unwrap().values() {
|
|
||||||
child.core.collect_into(&mut metrics);
|
|
||||||
}
|
|
||||||
m.set_metric(metrics);
|
|
||||||
|
|
||||||
vec![m]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> HyperLogLogVec<N> {
|
impl<const N: usize> MetricType for HyperLogLogState<N> {
|
||||||
/// Create a new [`HyperLogLogVec`] based on the provided
|
type Metadata = ();
|
||||||
/// [`Opts`] and partitioned by the given label names. At least one label name must be
|
|
||||||
/// provided.
|
|
||||||
pub fn new(opts: Opts, label_names: &[&str]) -> prometheus::Result<Self> {
|
|
||||||
assert!(N.is_power_of_two());
|
|
||||||
let variable_names = label_names.iter().map(|s| (*s).to_owned()).collect();
|
|
||||||
let opts = opts.variable_labels(variable_names);
|
|
||||||
|
|
||||||
let desc = opts.describe()?;
|
|
||||||
let v = HyperLogLogVecCore {
|
|
||||||
children: RwLock::new(HashMap::default()),
|
|
||||||
desc,
|
|
||||||
opts,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self { core: Arc::new(v) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `get_metric_with_label_values` returns the [`HyperLogLog<P>`] for the given slice
|
|
||||||
/// of label values (same order as the VariableLabels in Desc). If that combination of
|
|
||||||
/// label values is accessed for the first time, a new [`HyperLogLog<P>`] is created.
|
|
||||||
///
|
|
||||||
/// An error is returned if the number of label values is not the same as the
|
|
||||||
/// number of VariableLabels in Desc.
|
|
||||||
pub fn get_metric_with_label_values(
|
|
||||||
&self,
|
|
||||||
vals: &[&str],
|
|
||||||
) -> prometheus::Result<HyperLogLog<N>> {
|
|
||||||
self.core.get_metric_with_label_values(vals)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `with_label_values` works as `get_metric_with_label_values`, but panics if an error
|
|
||||||
/// occurs.
|
|
||||||
pub fn with_label_values(&self, vals: &[&str]) -> HyperLogLog<N> {
|
|
||||||
self.get_metric_with_label_values(vals).unwrap()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> HyperLogLogVecCore<N> {
|
impl<const N: usize> HyperLogLogState<N> {
|
||||||
pub fn get_metric_with_label_values(
|
|
||||||
&self,
|
|
||||||
vals: &[&str],
|
|
||||||
) -> prometheus::Result<HyperLogLog<N>> {
|
|
||||||
let h = self.hash_label_values(vals)?;
|
|
||||||
|
|
||||||
if let Some(metric) = self.children.read().unwrap().get(&h).cloned() {
|
|
||||||
return Ok(metric);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.get_or_create_metric(h, vals)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn hash_label_values(&self, vals: &[&str]) -> prometheus::Result<u64> {
|
|
||||||
if vals.len() != self.desc.variable_labels.len() {
|
|
||||||
return Err(prometheus::Error::InconsistentCardinality {
|
|
||||||
expect: self.desc.variable_labels.len(),
|
|
||||||
got: vals.len(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut h = xxh3::Hash64::default();
|
|
||||||
for val in vals {
|
|
||||||
h.write(val.as_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(h.finish())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_or_create_metric(
|
|
||||||
&self,
|
|
||||||
hash: u64,
|
|
||||||
label_values: &[&str],
|
|
||||||
) -> prometheus::Result<HyperLogLog<N>> {
|
|
||||||
let mut children = self.children.write().unwrap();
|
|
||||||
// Check exist first.
|
|
||||||
if let Some(metric) = children.get(&hash).cloned() {
|
|
||||||
return Ok(metric);
|
|
||||||
}
|
|
||||||
|
|
||||||
let metric = HyperLogLog::with_opts_and_label_values(&self.opts, label_values)?;
|
|
||||||
children.insert(hash, metric.clone());
|
|
||||||
Ok(metric)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// HLL is a probabilistic cardinality measure.
|
|
||||||
///
|
|
||||||
/// How to use this time-series for a metric name `my_metrics_total_hll`:
|
|
||||||
///
|
|
||||||
/// ```promql
|
|
||||||
/// # harmonic mean
|
|
||||||
/// 1 / (
|
|
||||||
/// sum (
|
|
||||||
/// 2 ^ -(
|
|
||||||
/// # HLL merge operation
|
|
||||||
/// max (my_metrics_total_hll{}) by (hll_shard, other_labels...)
|
|
||||||
/// )
|
|
||||||
/// ) without (hll_shard)
|
|
||||||
/// )
|
|
||||||
/// * alpha
|
|
||||||
/// * shards_count
|
|
||||||
/// * shards_count
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// If you want an estimate over time, you can use the following query:
|
|
||||||
///
|
|
||||||
/// ```promql
|
|
||||||
/// # harmonic mean
|
|
||||||
/// 1 / (
|
|
||||||
/// sum (
|
|
||||||
/// 2 ^ -(
|
|
||||||
/// # HLL merge operation
|
|
||||||
/// max (
|
|
||||||
/// max_over_time(my_metrics_total_hll{}[$__rate_interval])
|
|
||||||
/// ) by (hll_shard, other_labels...)
|
|
||||||
/// )
|
|
||||||
/// ) without (hll_shard)
|
|
||||||
/// )
|
|
||||||
/// * alpha
|
|
||||||
/// * shards_count
|
|
||||||
/// * shards_count
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// In the case of low cardinality, you might want to use the linear counting approximation:
|
|
||||||
///
|
|
||||||
/// ```promql
|
|
||||||
/// # LinearCounting(m, V) = m log (m / V)
|
|
||||||
/// shards_count * ln(shards_count /
|
|
||||||
/// # calculate V = how many shards contain a 0
|
|
||||||
/// count(max (proxy_connecting_endpoints{}) by (hll_shard, protocol) == 0) without (hll_shard)
|
|
||||||
/// )
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// See <https://en.wikipedia.org/wiki/HyperLogLog#Practical_considerations> for estimates on alpha
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct HyperLogLog<const N: usize> {
|
|
||||||
core: Arc<HyperLogLogCore<N>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> HyperLogLog<N> {
|
|
||||||
/// Create a [`HyperLogLog`] with the `name` and `help` arguments.
|
|
||||||
pub fn new<S1: Into<String>, S2: Into<String>>(name: S1, help: S2) -> prometheus::Result<Self> {
|
|
||||||
assert!(N.is_power_of_two());
|
|
||||||
let opts = Opts::new(name, help);
|
|
||||||
Self::with_opts(opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a [`HyperLogLog`] with the `opts` options.
|
|
||||||
pub fn with_opts(opts: Opts) -> prometheus::Result<Self> {
|
|
||||||
Self::with_opts_and_label_values(&opts, &[])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_opts_and_label_values(opts: &Opts, label_values: &[&str]) -> prometheus::Result<Self> {
|
|
||||||
let desc = opts.describe()?;
|
|
||||||
let labels = make_label_pairs(&desc, label_values)?;
|
|
||||||
|
|
||||||
let v = HyperLogLogCore {
|
|
||||||
shards: [0; N].map(AtomicU8::new),
|
|
||||||
desc,
|
|
||||||
labels,
|
|
||||||
};
|
|
||||||
Ok(Self { core: Arc::new(v) })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn measure(&self, item: &impl Hash) {
|
pub fn measure(&self, item: &impl Hash) {
|
||||||
// changing the hasher will break compatibility with previous measurements.
|
// changing the hasher will break compatibility with previous measurements.
|
||||||
self.record(BuildHasherDefault::<xxh3::Hash64>::default().hash_one(item));
|
self.record(BuildHasherDefault::<xxh3::Hash64>::default().hash_one(item));
|
||||||
@@ -299,42 +126,11 @@ impl<const N: usize> HyperLogLog<N> {
|
|||||||
let p = N.ilog2() as u8;
|
let p = N.ilog2() as u8;
|
||||||
let j = hash & (N as u64 - 1);
|
let j = hash & (N as u64 - 1);
|
||||||
let rho = (hash >> p).leading_zeros() as u8 + 1 - p;
|
let rho = (hash >> p).leading_zeros() as u8 + 1 - p;
|
||||||
self.core.shards[j as usize].fetch_max(rho, std::sync::atomic::Ordering::Relaxed);
|
self.shards[j as usize].fetch_max(rho, std::sync::atomic::Ordering::Relaxed);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct HyperLogLogCore<const N: usize> {
|
|
||||||
shards: [AtomicU8; N],
|
|
||||||
desc: core::Desc,
|
|
||||||
labels: Vec<proto::LabelPair>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> core::Collector for HyperLogLog<N> {
|
|
||||||
fn desc(&self) -> Vec<&core::Desc> {
|
|
||||||
vec![&self.core.desc]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect(&self) -> Vec<proto::MetricFamily> {
|
fn take_sample(&self) -> [u8; N] {
|
||||||
let mut m = proto::MetricFamily::default();
|
self.shards.each_ref().map(|x| {
|
||||||
m.set_name(self.core.desc.fq_name.clone());
|
|
||||||
m.set_help(self.core.desc.help.clone());
|
|
||||||
m.set_field_type(proto::MetricType::GAUGE);
|
|
||||||
|
|
||||||
let mut metrics = Vec::new();
|
|
||||||
self.core.collect_into(&mut metrics);
|
|
||||||
m.set_metric(metrics);
|
|
||||||
|
|
||||||
vec![m]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> HyperLogLogCore<N> {
|
|
||||||
fn collect_into(&self, metrics: &mut Vec<proto::Metric>) {
|
|
||||||
self.shards.iter().enumerate().for_each(|(i, x)| {
|
|
||||||
let mut shard_label = proto::LabelPair::default();
|
|
||||||
shard_label.set_name("hll_shard".to_owned());
|
|
||||||
shard_label.set_value(format!("{i}"));
|
|
||||||
|
|
||||||
// We reset the counter to 0 so we can perform a cardinality measure over any time slice in prometheus.
|
// We reset the counter to 0 so we can perform a cardinality measure over any time slice in prometheus.
|
||||||
|
|
||||||
// This seems like it would be a race condition,
|
// This seems like it would be a race condition,
|
||||||
@@ -344,85 +140,90 @@ impl<const N: usize> HyperLogLogCore<N> {
|
|||||||
|
|
||||||
// TODO: maybe we shouldn't reset this on every collect, instead, only after a time window.
|
// TODO: maybe we shouldn't reset this on every collect, instead, only after a time window.
|
||||||
// this would mean that a dev port-forwarding the metrics url won't break the sampling.
|
// this would mean that a dev port-forwarding the metrics url won't break the sampling.
|
||||||
let v = x.swap(0, std::sync::atomic::Ordering::Relaxed);
|
x.swap(0, std::sync::atomic::Ordering::Relaxed)
|
||||||
|
|
||||||
let mut m = proto::Metric::default();
|
|
||||||
let mut c = proto::Gauge::default();
|
|
||||||
c.set_value(v as f64);
|
|
||||||
m.set_gauge(c);
|
|
||||||
|
|
||||||
let mut labels = Vec::with_capacity(self.labels.len() + 1);
|
|
||||||
labels.extend_from_slice(&self.labels);
|
|
||||||
labels.push(shard_label);
|
|
||||||
|
|
||||||
m.set_label(labels);
|
|
||||||
metrics.push(m);
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl<W: std::io::Write, const N: usize> measured::metric::MetricEncoding<TextEncoder<W>>
|
||||||
fn make_label_pairs(
|
for HyperLogLogState<N>
|
||||||
desc: &core::Desc,
|
{
|
||||||
label_values: &[&str],
|
fn write_type(
|
||||||
) -> prometheus::Result<Vec<proto::LabelPair>> {
|
name: impl MetricNameEncoder,
|
||||||
if desc.variable_labels.len() != label_values.len() {
|
enc: &mut TextEncoder<W>,
|
||||||
return Err(prometheus::Error::InconsistentCardinality {
|
) -> Result<(), std::io::Error> {
|
||||||
expect: desc.variable_labels.len(),
|
enc.write_type(&name, measured::text::MetricType::Gauge)
|
||||||
got: label_values.len(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
fn collect_into(
|
||||||
|
&self,
|
||||||
|
_: &(),
|
||||||
|
labels: impl LabelGroup,
|
||||||
|
name: impl MetricNameEncoder,
|
||||||
|
enc: &mut TextEncoder<W>,
|
||||||
|
) -> Result<(), std::io::Error> {
|
||||||
|
struct I64(i64);
|
||||||
|
impl LabelValue for I64 {
|
||||||
|
fn visit<V: LabelVisitor>(&self, v: V) -> V::Output {
|
||||||
|
v.write_int(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let total_len = desc.variable_labels.len() + desc.const_label_pairs.len();
|
struct HllShardLabel {
|
||||||
if total_len == 0 {
|
hll_shard: i64,
|
||||||
return Ok(vec![]);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if desc.variable_labels.is_empty() {
|
impl LabelGroup for HllShardLabel {
|
||||||
return Ok(desc.const_label_pairs.clone());
|
fn visit_values(&self, v: &mut impl LabelGroupVisitor) {
|
||||||
}
|
const LE: &LabelName = LabelName::from_str("hll_shard");
|
||||||
|
v.write_value(LE, &I64(self.hll_shard));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut label_pairs = Vec::with_capacity(total_len);
|
self.take_sample()
|
||||||
for (i, n) in desc.variable_labels.iter().enumerate() {
|
.into_iter()
|
||||||
let mut label_pair = proto::LabelPair::default();
|
.enumerate()
|
||||||
label_pair.set_name(n.clone());
|
.try_for_each(|(hll_shard, val)| {
|
||||||
label_pair.set_value(label_values[i].to_owned());
|
enc.write_metric_value(
|
||||||
label_pairs.push(label_pair);
|
name.by_ref(),
|
||||||
|
labels.by_ref().compose_with(HllShardLabel {
|
||||||
|
hll_shard: hll_shard as i64,
|
||||||
|
}),
|
||||||
|
MetricValue::Int(val as i64),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
for label_pair in &desc.const_label_pairs {
|
|
||||||
label_pairs.push(label_pair.clone());
|
|
||||||
}
|
|
||||||
label_pairs.sort();
|
|
||||||
Ok(label_pairs)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use prometheus::{proto, Opts};
|
use measured::{label::StaticLabelSet, FixedCardinalityLabel};
|
||||||
use rand::{rngs::StdRng, Rng, SeedableRng};
|
use rand::{rngs::StdRng, Rng, SeedableRng};
|
||||||
use rand_distr::{Distribution, Zipf};
|
use rand_distr::{Distribution, Zipf};
|
||||||
|
|
||||||
use crate::HyperLogLogVec;
|
use crate::HyperLogLogVec;
|
||||||
|
|
||||||
fn collect(hll: &HyperLogLogVec<32>) -> Vec<proto::Metric> {
|
#[derive(FixedCardinalityLabel, Clone, Copy)]
|
||||||
let mut metrics = vec![];
|
#[label(singleton = "x")]
|
||||||
hll.core
|
enum Label {
|
||||||
.children
|
A,
|
||||||
.read()
|
B,
|
||||||
.unwrap()
|
|
||||||
.values()
|
|
||||||
.for_each(|c| c.core.collect_into(&mut metrics));
|
|
||||||
metrics
|
|
||||||
}
|
}
|
||||||
fn get_cardinality(metrics: &[proto::Metric], filter: impl Fn(&proto::Metric) -> bool) -> f64 {
|
|
||||||
|
fn collect(hll: &HyperLogLogVec<StaticLabelSet<Label>, 32>) -> ([u8; 32], [u8; 32]) {
|
||||||
|
// cannot go through the `hll.collect_family_into` interface yet...
|
||||||
|
// need to see if I can fix the conflicting impls problem in measured.
|
||||||
|
(
|
||||||
|
hll.get_metric(hll.with_labels(Label::A)).take_sample(),
|
||||||
|
hll.get_metric(hll.with_labels(Label::B)).take_sample(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cardinality(samples: &[[u8; 32]]) -> f64 {
|
||||||
let mut buckets = [0.0; 32];
|
let mut buckets = [0.0; 32];
|
||||||
for metric in metrics.chunks_exact(32) {
|
for &sample in samples {
|
||||||
if filter(&metric[0]) {
|
for (i, m) in sample.into_iter().enumerate() {
|
||||||
for (i, m) in metric.iter().enumerate() {
|
buckets[i] = f64::max(buckets[i], m as f64);
|
||||||
buckets[i] = f64::max(buckets[i], m.get_gauge().get_value());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +238,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn test_cardinality(n: usize, dist: impl Distribution<f64>) -> ([usize; 3], [f64; 3]) {
|
fn test_cardinality(n: usize, dist: impl Distribution<f64>) -> ([usize; 3], [f64; 3]) {
|
||||||
let hll = HyperLogLogVec::<32>::new(Opts::new("foo", "bar"), &["x"]).unwrap();
|
let hll = HyperLogLogVec::<StaticLabelSet<Label>, 32>::new();
|
||||||
|
|
||||||
let mut iter = StdRng::seed_from_u64(0x2024_0112).sample_iter(dist);
|
let mut iter = StdRng::seed_from_u64(0x2024_0112).sample_iter(dist);
|
||||||
let mut set_a = HashSet::new();
|
let mut set_a = HashSet::new();
|
||||||
@@ -445,18 +246,20 @@ mod tests {
|
|||||||
|
|
||||||
for x in iter.by_ref().take(n) {
|
for x in iter.by_ref().take(n) {
|
||||||
set_a.insert(x.to_bits());
|
set_a.insert(x.to_bits());
|
||||||
hll.with_label_values(&["a"]).measure(&x.to_bits());
|
hll.get_metric(hll.with_labels(Label::A))
|
||||||
|
.measure(&x.to_bits());
|
||||||
}
|
}
|
||||||
for x in iter.by_ref().take(n) {
|
for x in iter.by_ref().take(n) {
|
||||||
set_b.insert(x.to_bits());
|
set_b.insert(x.to_bits());
|
||||||
hll.with_label_values(&["b"]).measure(&x.to_bits());
|
hll.get_metric(hll.with_labels(Label::B))
|
||||||
|
.measure(&x.to_bits());
|
||||||
}
|
}
|
||||||
let merge = &set_a | &set_b;
|
let merge = &set_a | &set_b;
|
||||||
|
|
||||||
let metrics = collect(&hll);
|
let (a, b) = collect(&hll);
|
||||||
let len = get_cardinality(&metrics, |_| true);
|
let len = get_cardinality(&[a, b]);
|
||||||
let len_a = get_cardinality(&metrics, |l| l.get_label()[0].get_value() == "a");
|
let len_a = get_cardinality(&[a]);
|
||||||
let len_b = get_cardinality(&metrics, |l| l.get_label()[0].get_value() == "b");
|
let len_b = get_cardinality(&[b]);
|
||||||
|
|
||||||
([merge.len(), set_a.len(), set_b.len()], [len, len_a, len_b])
|
([merge.len(), set_a.len(), set_b.len()], [len, len_a, len_b])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,17 @@
|
|||||||
//! a default registry.
|
//! a default registry.
|
||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
|
|
||||||
|
use measured::{
|
||||||
|
label::{LabelGroupSet, LabelGroupVisitor, LabelName, NoLabels},
|
||||||
|
metric::{
|
||||||
|
counter::CounterState,
|
||||||
|
gauge::GaugeState,
|
||||||
|
group::{Encoding, MetricValue},
|
||||||
|
name::{MetricName, MetricNameEncoder},
|
||||||
|
MetricEncoding, MetricFamilyEncoding,
|
||||||
|
},
|
||||||
|
FixedCardinalityLabel, LabelGroup, MetricGroup,
|
||||||
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use prometheus::core::{
|
use prometheus::core::{
|
||||||
Atomic, AtomicU64, Collector, GenericCounter, GenericCounterVec, GenericGauge, GenericGaugeVec,
|
Atomic, AtomicU64, Collector, GenericCounter, GenericCounterVec, GenericGauge, GenericGaugeVec,
|
||||||
@@ -11,6 +22,7 @@ use prometheus::core::{
|
|||||||
pub use prometheus::opts;
|
pub use prometheus::opts;
|
||||||
pub use prometheus::register;
|
pub use prometheus::register;
|
||||||
pub use prometheus::Error;
|
pub use prometheus::Error;
|
||||||
|
use prometheus::Registry;
|
||||||
pub use prometheus::{core, default_registry, proto};
|
pub use prometheus::{core, default_registry, proto};
|
||||||
pub use prometheus::{exponential_buckets, linear_buckets};
|
pub use prometheus::{exponential_buckets, linear_buckets};
|
||||||
pub use prometheus::{register_counter_vec, Counter, CounterVec};
|
pub use prometheus::{register_counter_vec, Counter, CounterVec};
|
||||||
@@ -23,13 +35,12 @@ pub use prometheus::{register_int_counter_vec, IntCounterVec};
|
|||||||
pub use prometheus::{register_int_gauge, IntGauge};
|
pub use prometheus::{register_int_gauge, IntGauge};
|
||||||
pub use prometheus::{register_int_gauge_vec, IntGaugeVec};
|
pub use prometheus::{register_int_gauge_vec, IntGaugeVec};
|
||||||
pub use prometheus::{Encoder, TextEncoder};
|
pub use prometheus::{Encoder, TextEncoder};
|
||||||
use prometheus::{Registry, Result};
|
|
||||||
|
|
||||||
pub mod launch_timestamp;
|
pub mod launch_timestamp;
|
||||||
mod wrappers;
|
mod wrappers;
|
||||||
pub use wrappers::{CountedReader, CountedWriter};
|
pub use wrappers::{CountedReader, CountedWriter};
|
||||||
mod hll;
|
mod hll;
|
||||||
pub use hll::{HyperLogLog, HyperLogLogVec};
|
pub use hll::{HyperLogLog, HyperLogLogState, HyperLogLogVec};
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod more_process_metrics;
|
pub mod more_process_metrics;
|
||||||
|
|
||||||
@@ -59,7 +70,7 @@ static INTERNAL_REGISTRY: Lazy<Registry> = Lazy::new(Registry::new);
|
|||||||
/// Register a collector in the internal registry. MUST be called before the first call to `gather()`.
|
/// Register a collector in the internal registry. MUST be called before the first call to `gather()`.
|
||||||
/// Otherwise, we can have a deadlock in the `gather()` call, trying to register a new collector
|
/// Otherwise, we can have a deadlock in the `gather()` call, trying to register a new collector
|
||||||
/// while holding the lock.
|
/// while holding the lock.
|
||||||
pub fn register_internal(c: Box<dyn Collector>) -> Result<()> {
|
pub fn register_internal(c: Box<dyn Collector>) -> prometheus::Result<()> {
|
||||||
INTERNAL_REGISTRY.register(c)
|
INTERNAL_REGISTRY.register(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,9 +103,131 @@ static MAXRSS_KB: Lazy<IntGauge> = Lazy::new(|| {
|
|||||||
.expect("Failed to register maxrss_kb int gauge")
|
.expect("Failed to register maxrss_kb int gauge")
|
||||||
});
|
});
|
||||||
|
|
||||||
pub const DISK_WRITE_SECONDS_BUCKETS: &[f64] = &[
|
/// Most common fsync latency is 50 µs - 100 µs, but it can be much higher,
|
||||||
0.000_050, 0.000_100, 0.000_500, 0.001, 0.003, 0.005, 0.01, 0.05, 0.1, 0.3, 0.5,
|
/// especially during many concurrent disk operations.
|
||||||
];
|
pub const DISK_FSYNC_SECONDS_BUCKETS: &[f64] =
|
||||||
|
&[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0, 10.0, 30.0];
|
||||||
|
|
||||||
|
pub struct BuildInfo {
|
||||||
|
pub revision: &'static str,
|
||||||
|
pub build_tag: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo: allow label group without the set
|
||||||
|
impl LabelGroup for BuildInfo {
|
||||||
|
fn visit_values(&self, v: &mut impl LabelGroupVisitor) {
|
||||||
|
const REVISION: &LabelName = LabelName::from_str("revision");
|
||||||
|
v.write_value(REVISION, &self.revision);
|
||||||
|
const BUILD_TAG: &LabelName = LabelName::from_str("build_tag");
|
||||||
|
v.write_value(BUILD_TAG, &self.build_tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encoding> MetricFamilyEncoding<T> for BuildInfo
|
||||||
|
where
|
||||||
|
GaugeState: MetricEncoding<T>,
|
||||||
|
{
|
||||||
|
fn collect_family_into(
|
||||||
|
&self,
|
||||||
|
name: impl measured::metric::name::MetricNameEncoder,
|
||||||
|
enc: &mut T,
|
||||||
|
) -> Result<(), T::Err> {
|
||||||
|
enc.write_help(&name, "Build/version information")?;
|
||||||
|
GaugeState::write_type(&name, enc)?;
|
||||||
|
GaugeState {
|
||||||
|
count: std::sync::atomic::AtomicI64::new(1),
|
||||||
|
}
|
||||||
|
.collect_into(&(), self, name, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MetricGroup)]
|
||||||
|
#[metric(new(build_info: BuildInfo))]
|
||||||
|
pub struct NeonMetrics {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[metric(namespace = "process")]
|
||||||
|
#[metric(init = measured_process::ProcessCollector::for_self())]
|
||||||
|
process: measured_process::ProcessCollector,
|
||||||
|
|
||||||
|
#[metric(namespace = "libmetrics")]
|
||||||
|
#[metric(init = LibMetrics::new(build_info))]
|
||||||
|
libmetrics: LibMetrics,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MetricGroup)]
|
||||||
|
#[metric(new(build_info: BuildInfo))]
|
||||||
|
pub struct LibMetrics {
|
||||||
|
#[metric(init = build_info)]
|
||||||
|
build_info: BuildInfo,
|
||||||
|
|
||||||
|
#[metric(flatten)]
|
||||||
|
rusage: Rusage,
|
||||||
|
|
||||||
|
serve_count: CollectionCounter,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_gauge<Enc: Encoding>(
|
||||||
|
x: i64,
|
||||||
|
labels: impl LabelGroup,
|
||||||
|
name: impl MetricNameEncoder,
|
||||||
|
enc: &mut Enc,
|
||||||
|
) -> Result<(), Enc::Err> {
|
||||||
|
enc.write_metric_value(name, labels, MetricValue::Int(x))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct Rusage;
|
||||||
|
|
||||||
|
#[derive(FixedCardinalityLabel, Clone, Copy)]
|
||||||
|
#[label(singleton = "io_operation")]
|
||||||
|
enum IoOp {
|
||||||
|
Read,
|
||||||
|
Write,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encoding> MetricGroup<T> for Rusage
|
||||||
|
where
|
||||||
|
GaugeState: MetricEncoding<T>,
|
||||||
|
{
|
||||||
|
fn collect_group_into(&self, enc: &mut T) -> Result<(), T::Err> {
|
||||||
|
const DISK_IO: &MetricName = MetricName::from_str("disk_io_bytes_total");
|
||||||
|
const MAXRSS: &MetricName = MetricName::from_str("maxrss_kb");
|
||||||
|
|
||||||
|
let ru = get_rusage_stats();
|
||||||
|
|
||||||
|
enc.write_help(
|
||||||
|
DISK_IO,
|
||||||
|
"Bytes written and read from disk, grouped by the operation (read|write)",
|
||||||
|
)?;
|
||||||
|
GaugeState::write_type(DISK_IO, enc)?;
|
||||||
|
write_gauge(ru.ru_inblock * BYTES_IN_BLOCK, IoOp::Read, DISK_IO, enc)?;
|
||||||
|
write_gauge(ru.ru_oublock * BYTES_IN_BLOCK, IoOp::Write, DISK_IO, enc)?;
|
||||||
|
|
||||||
|
enc.write_help(MAXRSS, "Memory usage (Maximum Resident Set Size)")?;
|
||||||
|
GaugeState::write_type(MAXRSS, enc)?;
|
||||||
|
write_gauge(ru.ru_maxrss, IoOp::Read, MAXRSS, enc)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct CollectionCounter(CounterState);
|
||||||
|
|
||||||
|
impl<T: Encoding> MetricFamilyEncoding<T> for CollectionCounter
|
||||||
|
where
|
||||||
|
CounterState: MetricEncoding<T>,
|
||||||
|
{
|
||||||
|
fn collect_family_into(
|
||||||
|
&self,
|
||||||
|
name: impl measured::metric::name::MetricNameEncoder,
|
||||||
|
enc: &mut T,
|
||||||
|
) -> Result<(), T::Err> {
|
||||||
|
self.0.inc();
|
||||||
|
enc.write_help(&name, "Number of metric requests made")?;
|
||||||
|
self.0.collect_into(&(), NoLabels, name, enc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_build_info_metric(revision: &str, build_tag: &str) {
|
pub fn set_build_info_metric(revision: &str, build_tag: &str) {
|
||||||
let metric = register_int_gauge_vec!(
|
let metric = register_int_gauge_vec!(
|
||||||
@@ -105,6 +238,7 @@ pub fn set_build_info_metric(revision: &str, build_tag: &str) {
|
|||||||
.expect("Failed to register build info metric");
|
.expect("Failed to register build info metric");
|
||||||
metric.with_label_values(&[revision, build_tag]).set(1);
|
metric.with_label_values(&[revision, build_tag]).set(1);
|
||||||
}
|
}
|
||||||
|
const BYTES_IN_BLOCK: i64 = 512;
|
||||||
|
|
||||||
// Records I/O stats in a "cross-platform" way.
|
// Records I/O stats in a "cross-platform" way.
|
||||||
// Compiles both on macOS and Linux, but current macOS implementation always returns 0 as values for I/O stats.
|
// Compiles both on macOS and Linux, but current macOS implementation always returns 0 as values for I/O stats.
|
||||||
@@ -117,14 +251,22 @@ pub fn set_build_info_metric(revision: &str, build_tag: &str) {
|
|||||||
fn update_rusage_metrics() {
|
fn update_rusage_metrics() {
|
||||||
let rusage_stats = get_rusage_stats();
|
let rusage_stats = get_rusage_stats();
|
||||||
|
|
||||||
const BYTES_IN_BLOCK: i64 = 512;
|
|
||||||
DISK_IO_BYTES
|
DISK_IO_BYTES
|
||||||
.with_label_values(&["read"])
|
.with_label_values(&["read"])
|
||||||
.set(rusage_stats.ru_inblock * BYTES_IN_BLOCK);
|
.set(rusage_stats.ru_inblock * BYTES_IN_BLOCK);
|
||||||
DISK_IO_BYTES
|
DISK_IO_BYTES
|
||||||
.with_label_values(&["write"])
|
.with_label_values(&["write"])
|
||||||
.set(rusage_stats.ru_oublock * BYTES_IN_BLOCK);
|
.set(rusage_stats.ru_oublock * BYTES_IN_BLOCK);
|
||||||
MAXRSS_KB.set(rusage_stats.ru_maxrss);
|
|
||||||
|
// On macOS, the unit of maxrss is bytes; on Linux, it's kilobytes. https://stackoverflow.com/a/59915669
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
MAXRSS_KB.set(rusage_stats.ru_maxrss / 1024);
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
MAXRSS_KB.set(rusage_stats.ru_maxrss);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_rusage_stats() -> libc::rusage {
|
fn get_rusage_stats() -> libc::rusage {
|
||||||
@@ -151,6 +293,7 @@ macro_rules! register_int_counter_pair_vec {
|
|||||||
}
|
}
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create an [`IntCounterPair`] and registers to default registry.
|
/// Create an [`IntCounterPair`] and registers to default registry.
|
||||||
#[macro_export(local_inner_macros)]
|
#[macro_export(local_inner_macros)]
|
||||||
macro_rules! register_int_counter_pair {
|
macro_rules! register_int_counter_pair {
|
||||||
@@ -188,7 +331,10 @@ impl<P: Atomic> GenericCounterPairVec<P> {
|
|||||||
///
|
///
|
||||||
/// An error is returned if the number of label values is not the same as the
|
/// An error is returned if the number of label values is not the same as the
|
||||||
/// number of VariableLabels in Desc.
|
/// number of VariableLabels in Desc.
|
||||||
pub fn get_metric_with_label_values(&self, vals: &[&str]) -> Result<GenericCounterPair<P>> {
|
pub fn get_metric_with_label_values(
|
||||||
|
&self,
|
||||||
|
vals: &[&str],
|
||||||
|
) -> prometheus::Result<GenericCounterPair<P>> {
|
||||||
Ok(GenericCounterPair {
|
Ok(GenericCounterPair {
|
||||||
inc: self.inc.get_metric_with_label_values(vals)?,
|
inc: self.inc.get_metric_with_label_values(vals)?,
|
||||||
dec: self.dec.get_metric_with_label_values(vals)?,
|
dec: self.dec.get_metric_with_label_values(vals)?,
|
||||||
@@ -201,7 +347,7 @@ impl<P: Atomic> GenericCounterPairVec<P> {
|
|||||||
self.get_metric_with_label_values(vals).unwrap()
|
self.get_metric_with_label_values(vals).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_label_values(&self, res: &mut [Result<()>; 2], vals: &[&str]) {
|
pub fn remove_label_values(&self, res: &mut [prometheus::Result<()>; 2], vals: &[&str]) {
|
||||||
res[0] = self.inc.remove_label_values(vals);
|
res[0] = self.inc.remove_label_values(vals);
|
||||||
res[1] = self.dec.remove_label_values(vals);
|
res[1] = self.dec.remove_label_values(vals);
|
||||||
}
|
}
|
||||||
@@ -285,3 +431,180 @@ pub type IntCounterPair = GenericCounterPair<AtomicU64>;
|
|||||||
|
|
||||||
/// A guard for [`IntCounterPair`] that will decrement the gauge on drop
|
/// A guard for [`IntCounterPair`] that will decrement the gauge on drop
|
||||||
pub type IntCounterPairGuard = GenericCounterPairGuard<AtomicU64>;
|
pub type IntCounterPairGuard = GenericCounterPairGuard<AtomicU64>;
|
||||||
|
|
||||||
|
pub trait CounterPairAssoc {
|
||||||
|
const INC_NAME: &'static MetricName;
|
||||||
|
const DEC_NAME: &'static MetricName;
|
||||||
|
|
||||||
|
const INC_HELP: &'static str;
|
||||||
|
const DEC_HELP: &'static str;
|
||||||
|
|
||||||
|
type LabelGroupSet: LabelGroupSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CounterPairVec<A: CounterPairAssoc> {
|
||||||
|
vec: measured::metric::MetricVec<MeasuredCounterPairState, A::LabelGroupSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: CounterPairAssoc> Default for CounterPairVec<A>
|
||||||
|
where
|
||||||
|
A::LabelGroupSet: Default,
|
||||||
|
{
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
vec: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: CounterPairAssoc> CounterPairVec<A> {
|
||||||
|
pub fn guard(
|
||||||
|
&self,
|
||||||
|
labels: <A::LabelGroupSet as LabelGroupSet>::Group<'_>,
|
||||||
|
) -> MeasuredCounterPairGuard<'_, A> {
|
||||||
|
let id = self.vec.with_labels(labels);
|
||||||
|
self.vec.get_metric(id).inc.inc();
|
||||||
|
MeasuredCounterPairGuard { vec: &self.vec, id }
|
||||||
|
}
|
||||||
|
pub fn inc(&self, labels: <A::LabelGroupSet as LabelGroupSet>::Group<'_>) {
|
||||||
|
let id = self.vec.with_labels(labels);
|
||||||
|
self.vec.get_metric(id).inc.inc();
|
||||||
|
}
|
||||||
|
pub fn dec(&self, labels: <A::LabelGroupSet as LabelGroupSet>::Group<'_>) {
|
||||||
|
let id = self.vec.with_labels(labels);
|
||||||
|
self.vec.get_metric(id).dec.inc();
|
||||||
|
}
|
||||||
|
pub fn remove_metric(
|
||||||
|
&self,
|
||||||
|
labels: <A::LabelGroupSet as LabelGroupSet>::Group<'_>,
|
||||||
|
) -> Option<MeasuredCounterPairState> {
|
||||||
|
let id = self.vec.with_labels(labels);
|
||||||
|
self.vec.remove_metric(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sample(&self, labels: <A::LabelGroupSet as LabelGroupSet>::Group<'_>) -> u64 {
|
||||||
|
let id = self.vec.with_labels(labels);
|
||||||
|
let metric = self.vec.get_metric(id);
|
||||||
|
|
||||||
|
let inc = metric.inc.count.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
let dec = metric.dec.count.load(std::sync::atomic::Ordering::Relaxed);
|
||||||
|
inc.saturating_sub(dec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, A> ::measured::metric::group::MetricGroup<T> for CounterPairVec<A>
|
||||||
|
where
|
||||||
|
T: ::measured::metric::group::Encoding,
|
||||||
|
A: CounterPairAssoc,
|
||||||
|
::measured::metric::counter::CounterState: ::measured::metric::MetricEncoding<T>,
|
||||||
|
{
|
||||||
|
fn collect_group_into(&self, enc: &mut T) -> Result<(), T::Err> {
|
||||||
|
// write decrement first to avoid a race condition where inc - dec < 0
|
||||||
|
T::write_help(enc, A::DEC_NAME, A::DEC_HELP)?;
|
||||||
|
self.vec
|
||||||
|
.collect_family_into(A::DEC_NAME, &mut Dec(&mut *enc))?;
|
||||||
|
|
||||||
|
T::write_help(enc, A::INC_NAME, A::INC_HELP)?;
|
||||||
|
self.vec
|
||||||
|
.collect_family_into(A::INC_NAME, &mut Inc(&mut *enc))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(MetricGroup, Default)]
|
||||||
|
pub struct MeasuredCounterPairState {
|
||||||
|
pub inc: CounterState,
|
||||||
|
pub dec: CounterState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl measured::metric::MetricType for MeasuredCounterPairState {
|
||||||
|
type Metadata = ();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MeasuredCounterPairGuard<'a, A: CounterPairAssoc> {
|
||||||
|
vec: &'a measured::metric::MetricVec<MeasuredCounterPairState, A::LabelGroupSet>,
|
||||||
|
id: measured::metric::LabelId<A::LabelGroupSet>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: CounterPairAssoc> Drop for MeasuredCounterPairGuard<'_, A> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.vec.get_metric(self.id).dec.inc();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`MetricEncoding`] for [`MeasuredCounterPairState`] that only writes the inc counter to the inner encoder.
|
||||||
|
struct Inc<T>(T);
|
||||||
|
/// [`MetricEncoding`] for [`MeasuredCounterPairState`] that only writes the dec counter to the inner encoder.
|
||||||
|
struct Dec<T>(T);
|
||||||
|
|
||||||
|
impl<T: Encoding> Encoding for Inc<T> {
|
||||||
|
type Err = T::Err;
|
||||||
|
|
||||||
|
fn write_help(&mut self, name: impl MetricNameEncoder, help: &str) -> Result<(), Self::Err> {
|
||||||
|
self.0.write_help(name, help)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_metric_value(
|
||||||
|
&mut self,
|
||||||
|
name: impl MetricNameEncoder,
|
||||||
|
labels: impl LabelGroup,
|
||||||
|
value: MetricValue,
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
self.0.write_metric_value(name, labels, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encoding> MetricEncoding<Inc<T>> for MeasuredCounterPairState
|
||||||
|
where
|
||||||
|
CounterState: MetricEncoding<T>,
|
||||||
|
{
|
||||||
|
fn write_type(name: impl MetricNameEncoder, enc: &mut Inc<T>) -> Result<(), T::Err> {
|
||||||
|
CounterState::write_type(name, &mut enc.0)
|
||||||
|
}
|
||||||
|
fn collect_into(
|
||||||
|
&self,
|
||||||
|
metadata: &(),
|
||||||
|
labels: impl LabelGroup,
|
||||||
|
name: impl MetricNameEncoder,
|
||||||
|
enc: &mut Inc<T>,
|
||||||
|
) -> Result<(), T::Err> {
|
||||||
|
self.inc.collect_into(metadata, labels, name, &mut enc.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Encoding> Encoding for Dec<T> {
|
||||||
|
type Err = T::Err;
|
||||||
|
|
||||||
|
fn write_help(&mut self, name: impl MetricNameEncoder, help: &str) -> Result<(), Self::Err> {
|
||||||
|
self.0.write_help(name, help)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_metric_value(
|
||||||
|
&mut self,
|
||||||
|
name: impl MetricNameEncoder,
|
||||||
|
labels: impl LabelGroup,
|
||||||
|
value: MetricValue,
|
||||||
|
) -> Result<(), Self::Err> {
|
||||||
|
self.0.write_metric_value(name, labels, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write the dec counter to the encoder
|
||||||
|
impl<T: Encoding> MetricEncoding<Dec<T>> for MeasuredCounterPairState
|
||||||
|
where
|
||||||
|
CounterState: MetricEncoding<T>,
|
||||||
|
{
|
||||||
|
fn write_type(name: impl MetricNameEncoder, enc: &mut Dec<T>) -> Result<(), T::Err> {
|
||||||
|
CounterState::write_type(name, &mut enc.0)
|
||||||
|
}
|
||||||
|
fn collect_into(
|
||||||
|
&self,
|
||||||
|
metadata: &(),
|
||||||
|
labels: impl LabelGroup,
|
||||||
|
name: impl MetricNameEncoder,
|
||||||
|
enc: &mut Dec<T>,
|
||||||
|
) -> Result<(), T::Err> {
|
||||||
|
self.dec.collect_into(metadata, labels, name, &mut enc.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
31
libs/pageserver_api/src/config.rs
Normal file
31
libs/pageserver_api/src/config.rs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use const_format::formatcp;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
pub const DEFAULT_PG_LISTEN_PORT: u16 = 64000;
|
||||||
|
pub const DEFAULT_PG_LISTEN_ADDR: &str = formatcp!("127.0.0.1:{DEFAULT_PG_LISTEN_PORT}");
|
||||||
|
pub const DEFAULT_HTTP_LISTEN_PORT: u16 = 9898;
|
||||||
|
pub const DEFAULT_HTTP_LISTEN_ADDR: &str = formatcp!("127.0.0.1:{DEFAULT_HTTP_LISTEN_PORT}");
|
||||||
|
|
||||||
|
// Certain metadata (e.g. externally-addressable name, AZ) is delivered
|
||||||
|
// as a separate structure. This information is not neeed by the pageserver
|
||||||
|
// itself, it is only used for registering the pageserver with the control
|
||||||
|
// plane and/or storage controller.
|
||||||
|
//
|
||||||
|
#[derive(PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct NodeMetadata {
|
||||||
|
#[serde(rename = "host")]
|
||||||
|
pub postgres_host: String,
|
||||||
|
#[serde(rename = "port")]
|
||||||
|
pub postgres_port: u16,
|
||||||
|
pub http_host: String,
|
||||||
|
pub http_port: u16,
|
||||||
|
|
||||||
|
// Deployment tools may write fields to the metadata file beyond what we
|
||||||
|
// use in this type: this type intentionally only names fields that require.
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub other: HashMap<String, serde_json::Value>,
|
||||||
|
}
|
||||||
22
libs/pageserver_api/src/config/tests.rs
Normal file
22
libs/pageserver_api/src/config/tests.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_node_metadata_v1_backward_compatibilty() {
|
||||||
|
let v1 = serde_json::to_vec(&serde_json::json!({
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 23,
|
||||||
|
"http_host": "localhost",
|
||||||
|
"http_port": 42,
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
serde_json::from_slice::<NodeMetadata>(&v1.unwrap()).unwrap(),
|
||||||
|
NodeMetadata {
|
||||||
|
postgres_host: "localhost".to_string(),
|
||||||
|
postgres_port: 23,
|
||||||
|
http_host: "localhost".to_string(),
|
||||||
|
http_port: 42,
|
||||||
|
other: HashMap::new(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ use std::str::FromStr;
|
|||||||
|
|
||||||
/// Request/response types for the storage controller
|
/// Request/response types for the storage controller
|
||||||
/// API (`/control/v1` prefix). Implemented by the server
|
/// API (`/control/v1` prefix). Implemented by the server
|
||||||
/// in [`attachment_service::http`]
|
/// in [`storage_controller::http`]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utils::id::NodeId;
|
use utils::id::{NodeId, TenantId};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{ShardParameters, TenantConfig},
|
models::{ShardParameters, TenantConfig},
|
||||||
@@ -42,6 +42,12 @@ pub struct NodeConfigureRequest {
|
|||||||
pub scheduling: Option<NodeSchedulingPolicy>,
|
pub scheduling: Option<NodeSchedulingPolicy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct TenantPolicyRequest {
|
||||||
|
pub placement: Option<PlacementPolicy>,
|
||||||
|
pub scheduling: Option<ShardSchedulingPolicy>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct TenantLocateResponseShard {
|
pub struct TenantLocateResponseShard {
|
||||||
pub shard_id: TenantShardId,
|
pub shard_id: TenantShardId,
|
||||||
@@ -62,12 +68,27 @@ pub struct TenantLocateResponse {
|
|||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct TenantDescribeResponse {
|
pub struct TenantDescribeResponse {
|
||||||
|
pub tenant_id: TenantId,
|
||||||
pub shards: Vec<TenantDescribeResponseShard>,
|
pub shards: Vec<TenantDescribeResponseShard>,
|
||||||
pub stripe_size: ShardStripeSize,
|
pub stripe_size: ShardStripeSize,
|
||||||
pub policy: PlacementPolicy,
|
pub policy: PlacementPolicy,
|
||||||
pub config: TenantConfig,
|
pub config: TenantConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct NodeDescribeResponse {
|
||||||
|
pub id: NodeId,
|
||||||
|
|
||||||
|
pub availability: NodeAvailabilityWrapper,
|
||||||
|
pub scheduling: NodeSchedulingPolicy,
|
||||||
|
|
||||||
|
pub listen_http_addr: String,
|
||||||
|
pub listen_http_port: u16,
|
||||||
|
|
||||||
|
pub listen_pg_addr: String,
|
||||||
|
pub listen_pg_port: u16,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct TenantDescribeResponseShard {
|
pub struct TenantDescribeResponseShard {
|
||||||
pub tenant_shard_id: TenantShardId,
|
pub tenant_shard_id: TenantShardId,
|
||||||
@@ -83,6 +104,8 @@ pub struct TenantDescribeResponseShard {
|
|||||||
pub is_pending_compute_notification: bool,
|
pub is_pending_compute_notification: bool,
|
||||||
/// A shard split is currently underway
|
/// A shard split is currently underway
|
||||||
pub is_splitting: bool,
|
pub is_splitting: bool,
|
||||||
|
|
||||||
|
pub scheduling_policy: ShardSchedulingPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Explicitly migrating a particular shard is a low level operation
|
/// Explicitly migrating a particular shard is a low level operation
|
||||||
@@ -97,7 +120,7 @@ pub struct TenantShardMigrateRequest {
|
|||||||
/// Utilisation score indicating how good a candidate a pageserver
|
/// Utilisation score indicating how good a candidate a pageserver
|
||||||
/// is for scheduling the next tenant. See [`crate::models::PageserverUtilization`].
|
/// is for scheduling the next tenant. See [`crate::models::PageserverUtilization`].
|
||||||
/// Lower values are better.
|
/// Lower values are better.
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Debug)]
|
||||||
pub struct UtilizationScore(pub u64);
|
pub struct UtilizationScore(pub u64);
|
||||||
|
|
||||||
impl UtilizationScore {
|
impl UtilizationScore {
|
||||||
@@ -106,7 +129,7 @@ impl UtilizationScore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone, Copy)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
#[serde(into = "NodeAvailabilityWrapper")]
|
#[serde(into = "NodeAvailabilityWrapper")]
|
||||||
pub enum NodeAvailability {
|
pub enum NodeAvailability {
|
||||||
// Normal, happy state
|
// Normal, happy state
|
||||||
@@ -129,7 +152,7 @@ impl Eq for NodeAvailability {}
|
|||||||
// This wrapper provides serde functionality and it should only be used to
|
// This wrapper provides serde functionality and it should only be used to
|
||||||
// communicate with external callers which don't know or care about the
|
// communicate with external callers which don't know or care about the
|
||||||
// utilisation score of the pageserver it is targeting.
|
// utilisation score of the pageserver it is targeting.
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Debug)]
|
||||||
pub enum NodeAvailabilityWrapper {
|
pub enum NodeAvailabilityWrapper {
|
||||||
Active,
|
Active,
|
||||||
Offline,
|
Offline,
|
||||||
@@ -155,26 +178,38 @@ impl From<NodeAvailability> for NodeAvailabilityWrapper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for NodeAvailability {
|
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
type Err = anyhow::Error;
|
pub enum ShardSchedulingPolicy {
|
||||||
|
// Normal mode: the tenant's scheduled locations may be updated at will, including
|
||||||
|
// for non-essential optimization.
|
||||||
|
Active,
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
// Disable optimizations, but permit scheduling when necessary to fulfil the PlacementPolicy.
|
||||||
match s {
|
// For example, this still permits a node's attachment location to change to a secondary in
|
||||||
// This is used when parsing node configuration requests from neon-local.
|
// response to a node failure, or to assign a new secondary if a node was removed.
|
||||||
// Assume the worst possible utilisation score
|
Essential,
|
||||||
// and let it get updated via the heartbeats.
|
|
||||||
"active" => Ok(Self::Active(UtilizationScore::worst())),
|
// No scheduling: leave the shard running wherever it currently is. Even if the shard is
|
||||||
"offline" => Ok(Self::Offline),
|
// unavailable, it will not be rescheduled to another node.
|
||||||
_ => Err(anyhow::anyhow!("Unknown availability state '{s}'")),
|
Pause,
|
||||||
}
|
|
||||||
|
// No reconciling: we will make no location_conf API calls to pageservers at all. If the
|
||||||
|
// shard is unavailable, it stays that way. If a node fails, this shard doesn't get failed over.
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ShardSchedulingPolicy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Active
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
pub enum NodeSchedulingPolicy {
|
pub enum NodeSchedulingPolicy {
|
||||||
Active,
|
Active,
|
||||||
Filling,
|
Filling,
|
||||||
Pause,
|
Pause,
|
||||||
|
PauseForRestart,
|
||||||
Draining,
|
Draining,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,6 +221,7 @@ impl FromStr for NodeSchedulingPolicy {
|
|||||||
"active" => Ok(Self::Active),
|
"active" => Ok(Self::Active),
|
||||||
"filling" => Ok(Self::Filling),
|
"filling" => Ok(Self::Filling),
|
||||||
"pause" => Ok(Self::Pause),
|
"pause" => Ok(Self::Pause),
|
||||||
|
"pause_for_restart" => Ok(Self::PauseForRestart),
|
||||||
"draining" => Ok(Self::Draining),
|
"draining" => Ok(Self::Draining),
|
||||||
_ => Err(anyhow::anyhow!("Unknown scheduling state '{s}'")),
|
_ => Err(anyhow::anyhow!("Unknown scheduling state '{s}'")),
|
||||||
}
|
}
|
||||||
@@ -199,6 +235,7 @@ impl From<NodeSchedulingPolicy> for String {
|
|||||||
Active => "active",
|
Active => "active",
|
||||||
Filling => "filling",
|
Filling => "filling",
|
||||||
Pause => "pause",
|
Pause => "pause",
|
||||||
|
PauseForRestart => "pause_for_restart",
|
||||||
Draining => "draining",
|
Draining => "draining",
|
||||||
}
|
}
|
||||||
.to_string()
|
.to_string()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
use byteorder::{ByteOrder, BE};
|
use byteorder::{ByteOrder, BE};
|
||||||
use postgres_ffi::relfile_utils::{FSM_FORKNUM, VISIBILITYMAP_FORKNUM};
|
use postgres_ffi::relfile_utils::{FSM_FORKNUM, VISIBILITYMAP_FORKNUM};
|
||||||
|
use postgres_ffi::RepOriginId;
|
||||||
use postgres_ffi::{Oid, TransactionId};
|
use postgres_ffi::{Oid, TransactionId};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{fmt, ops::Range};
|
use std::{fmt, ops::Range};
|
||||||
@@ -21,15 +22,93 @@ pub struct Key {
|
|||||||
pub field6: u32,
|
pub field6: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The storage key size.
|
||||||
pub const KEY_SIZE: usize = 18;
|
pub const KEY_SIZE: usize = 18;
|
||||||
|
|
||||||
|
/// The metadata key size. 2B fewer than the storage key size because field2 is not fully utilized.
|
||||||
|
/// See [`Key::to_i128`] for more information on the encoding.
|
||||||
|
pub const METADATA_KEY_SIZE: usize = 16;
|
||||||
|
|
||||||
|
/// The key prefix start range for the metadata keys. All keys with the first byte >= 0x40 is a metadata key.
|
||||||
|
pub const METADATA_KEY_BEGIN_PREFIX: u8 = 0x60;
|
||||||
|
pub const METADATA_KEY_END_PREFIX: u8 = 0x7F;
|
||||||
|
|
||||||
|
/// The (reserved) key prefix of relation sizes.
|
||||||
|
pub const RELATION_SIZE_PREFIX: u8 = 0x61;
|
||||||
|
|
||||||
|
/// The key prefix of AUX file keys.
|
||||||
|
pub const AUX_KEY_PREFIX: u8 = 0x62;
|
||||||
|
|
||||||
|
/// The key prefix of ReplOrigin keys.
|
||||||
|
pub const REPL_ORIGIN_KEY_PREFIX: u8 = 0x63;
|
||||||
|
|
||||||
|
/// Check if the key falls in the range of metadata keys.
|
||||||
|
pub const fn is_metadata_key_slice(key: &[u8]) -> bool {
|
||||||
|
key[0] >= METADATA_KEY_BEGIN_PREFIX && key[0] < METADATA_KEY_END_PREFIX
|
||||||
|
}
|
||||||
|
|
||||||
impl Key {
|
impl Key {
|
||||||
|
/// Check if the key falls in the range of metadata keys.
|
||||||
|
pub const fn is_metadata_key(&self) -> bool {
|
||||||
|
self.field1 >= METADATA_KEY_BEGIN_PREFIX && self.field1 < METADATA_KEY_END_PREFIX
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a metadata key to a storage key.
|
||||||
|
pub fn from_metadata_key_fixed_size(key: &[u8; METADATA_KEY_SIZE]) -> Self {
|
||||||
|
assert!(is_metadata_key_slice(key), "key not in metadata key range");
|
||||||
|
// Metadata key space ends at 0x7F so it's fine to directly convert it to i128.
|
||||||
|
Self::from_i128(i128::from_be_bytes(*key))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode a metadata key to a storage key.
|
||||||
|
pub fn from_metadata_key(key: &[u8]) -> Self {
|
||||||
|
Self::from_metadata_key_fixed_size(key.try_into().expect("expect 16 byte metadata key"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the range of metadata keys.
|
||||||
|
pub const fn metadata_key_range() -> Range<Self> {
|
||||||
|
Key {
|
||||||
|
field1: METADATA_KEY_BEGIN_PREFIX,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0,
|
||||||
|
field5: 0,
|
||||||
|
field6: 0,
|
||||||
|
}..Key {
|
||||||
|
field1: METADATA_KEY_END_PREFIX,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0,
|
||||||
|
field5: 0,
|
||||||
|
field6: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the range of aux keys.
|
||||||
|
pub fn metadata_aux_key_range() -> Range<Self> {
|
||||||
|
Key {
|
||||||
|
field1: AUX_KEY_PREFIX,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0,
|
||||||
|
field5: 0,
|
||||||
|
field6: 0,
|
||||||
|
}..Key {
|
||||||
|
field1: AUX_KEY_PREFIX + 1,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0,
|
||||||
|
field5: 0,
|
||||||
|
field6: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 'field2' is used to store tablespaceid for relations and small enum numbers for other relish.
|
/// 'field2' is used to store tablespaceid for relations and small enum numbers for other relish.
|
||||||
/// As long as Neon does not support tablespace (because of lack of access to local file system),
|
/// As long as Neon does not support tablespace (because of lack of access to local file system),
|
||||||
/// we can assume that only some predefined namespace OIDs are used which can fit in u16
|
/// we can assume that only some predefined namespace OIDs are used which can fit in u16
|
||||||
pub fn to_i128(&self) -> i128 {
|
pub fn to_i128(&self) -> i128 {
|
||||||
assert!(self.field2 < 0xFFFF || self.field2 == 0xFFFFFFFF || self.field2 == 0x22222222);
|
assert!(self.field2 <= 0xFFFF || self.field2 == 0xFFFFFFFF || self.field2 == 0x22222222);
|
||||||
(((self.field1 & 0xf) as i128) << 120)
|
(((self.field1 & 0x7F) as i128) << 120)
|
||||||
| (((self.field2 & 0xFFFF) as i128) << 104)
|
| (((self.field2 & 0xFFFF) as i128) << 104)
|
||||||
| ((self.field3 as i128) << 72)
|
| ((self.field3 as i128) << 72)
|
||||||
| ((self.field4 as i128) << 40)
|
| ((self.field4 as i128) << 40)
|
||||||
@@ -39,7 +118,7 @@ impl Key {
|
|||||||
|
|
||||||
pub const fn from_i128(x: i128) -> Self {
|
pub const fn from_i128(x: i128) -> Self {
|
||||||
Key {
|
Key {
|
||||||
field1: ((x >> 120) & 0xf) as u8,
|
field1: ((x >> 120) & 0x7F) as u8,
|
||||||
field2: ((x >> 104) & 0xFFFF) as u32,
|
field2: ((x >> 104) & 0xFFFF) as u32,
|
||||||
field3: (x >> 72) as u32,
|
field3: (x >> 72) as u32,
|
||||||
field4: (x >> 40) as u32,
|
field4: (x >> 40) as u32,
|
||||||
@@ -48,11 +127,11 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next(&self) -> Key {
|
pub const fn next(&self) -> Key {
|
||||||
self.add(1)
|
self.add(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add(&self, x: u32) -> Key {
|
pub const fn add(&self, x: u32) -> Key {
|
||||||
let mut key = *self;
|
let mut key = *self;
|
||||||
|
|
||||||
let r = key.field6.overflowing_add(x);
|
let r = key.field6.overflowing_add(x);
|
||||||
@@ -81,6 +160,9 @@ impl Key {
|
|||||||
key
|
key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a 18B slice to a key. This function should not be used for 16B metadata keys because `field2` is handled differently.
|
||||||
|
/// Use [`Key::from_i128`] instead if you want to handle 16B keys (i.e., metadata keys). There are some restrictions on `field2`,
|
||||||
|
/// and therefore not all 18B slices are valid page server keys.
|
||||||
pub fn from_slice(b: &[u8]) -> Self {
|
pub fn from_slice(b: &[u8]) -> Self {
|
||||||
Key {
|
Key {
|
||||||
field1: b[0],
|
field1: b[0],
|
||||||
@@ -92,6 +174,8 @@ impl Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a key to a 18B slice. This function should not be used for getting a 16B metadata key because `field2` is handled differently.
|
||||||
|
/// Use [`Key::to_i128`] instead if you want to get a 16B key (i.e., metadata keys).
|
||||||
pub fn write_to_byte_slice(&self, buf: &mut [u8]) {
|
pub fn write_to_byte_slice(&self, buf: &mut [u8]) {
|
||||||
buf[0] = self.field1;
|
buf[0] = self.field1;
|
||||||
BE::write_u32(&mut buf[1..5], self.field2);
|
BE::write_u32(&mut buf[1..5], self.field2);
|
||||||
@@ -302,7 +386,14 @@ pub fn rel_size_to_key(rel: RelTag) -> Key {
|
|||||||
field3: rel.dbnode,
|
field3: rel.dbnode,
|
||||||
field4: rel.relnode,
|
field4: rel.relnode,
|
||||||
field5: rel.forknum,
|
field5: rel.forknum,
|
||||||
field6: 0xffffffff,
|
field6: 0xffff_ffff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Key {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_rel_size_key(&self) -> bool {
|
||||||
|
self.field1 == 0 && self.field6 == u32::MAX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +434,25 @@ pub fn slru_dir_to_key(kind: SlruKind) -> Key {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn slru_dir_kind(key: &Key) -> Option<Result<SlruKind, u32>> {
|
||||||
|
if key.field1 == 0x01
|
||||||
|
&& key.field3 == 0
|
||||||
|
&& key.field4 == 0
|
||||||
|
&& key.field5 == 0
|
||||||
|
&& key.field6 == 0
|
||||||
|
{
|
||||||
|
match key.field2 {
|
||||||
|
0 => Some(Ok(SlruKind::Clog)),
|
||||||
|
1 => Some(Ok(SlruKind::MultiXactMembers)),
|
||||||
|
2 => Some(Ok(SlruKind::MultiXactOffsets)),
|
||||||
|
x => Some(Err(x)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn slru_block_to_key(kind: SlruKind, segno: u32, blknum: BlockNumber) -> Key {
|
pub fn slru_block_to_key(kind: SlruKind, segno: u32, blknum: BlockNumber) -> Key {
|
||||||
Key {
|
Key {
|
||||||
@@ -371,7 +481,17 @@ pub fn slru_segment_size_to_key(kind: SlruKind, segno: u32) -> Key {
|
|||||||
field3: 1,
|
field3: 1,
|
||||||
field4: segno,
|
field4: segno,
|
||||||
field5: 0,
|
field5: 0,
|
||||||
field6: 0xffffffff,
|
field6: 0xffff_ffff,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Key {
|
||||||
|
pub fn is_slru_segment_size_key(&self) -> bool {
|
||||||
|
self.field1 == 0x01
|
||||||
|
&& self.field2 < 0x03
|
||||||
|
&& self.field3 == 0x01
|
||||||
|
&& self.field5 == 0
|
||||||
|
&& self.field6 == u32::MAX
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,76 +592,117 @@ pub const AUX_FILES_KEY: Key = Key {
|
|||||||
field6: 2,
|
field6: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn repl_origin_key(origin_id: RepOriginId) -> Key {
|
||||||
|
Key {
|
||||||
|
field1: REPL_ORIGIN_KEY_PREFIX,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0,
|
||||||
|
field5: 0,
|
||||||
|
field6: origin_id as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the range of replorigin keys.
|
||||||
|
pub fn repl_origin_key_range() -> Range<Key> {
|
||||||
|
Key {
|
||||||
|
field1: REPL_ORIGIN_KEY_PREFIX,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0,
|
||||||
|
field5: 0,
|
||||||
|
field6: 0,
|
||||||
|
}..Key {
|
||||||
|
field1: REPL_ORIGIN_KEY_PREFIX,
|
||||||
|
field2: 0,
|
||||||
|
field3: 0,
|
||||||
|
field4: 0,
|
||||||
|
field5: 0,
|
||||||
|
field6: 0x10000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reverse mappings for a few Keys.
|
// Reverse mappings for a few Keys.
|
||||||
// These are needed by WAL redo manager.
|
// These are needed by WAL redo manager.
|
||||||
|
|
||||||
// AUX_FILES currently stores only data for logical replication (slots etc), and
|
/// Non inherited range for vectored get.
|
||||||
// we don't preserve these on a branch because safekeepers can't follow timeline
|
pub const NON_INHERITED_RANGE: Range<Key> = AUX_FILES_KEY..AUX_FILES_KEY.next();
|
||||||
// switch (and generally it likely should be optional), so ignore these.
|
/// Sparse keyspace range for vectored get. Missing key error will be ignored for this range.
|
||||||
#[inline(always)]
|
pub const NON_INHERITED_SPARSE_RANGE: Range<Key> = Key::metadata_key_range();
|
||||||
pub fn is_inherited_key(key: Key) -> bool {
|
|
||||||
key != AUX_FILES_KEY
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
impl Key {
|
||||||
pub fn is_rel_fsm_block_key(key: Key) -> bool {
|
// AUX_FILES currently stores only data for logical replication (slots etc), and
|
||||||
key.field1 == 0x00 && key.field4 != 0 && key.field5 == FSM_FORKNUM && key.field6 != 0xffffffff
|
// we don't preserve these on a branch because safekeepers can't follow timeline
|
||||||
}
|
// switch (and generally it likely should be optional), so ignore these.
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn is_inherited_key(self) -> bool {
|
||||||
|
!NON_INHERITED_RANGE.contains(&self) && !NON_INHERITED_SPARSE_RANGE.contains(&self)
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_rel_vm_block_key(key: Key) -> bool {
|
pub fn is_rel_fsm_block_key(self) -> bool {
|
||||||
key.field1 == 0x00
|
self.field1 == 0x00
|
||||||
&& key.field4 != 0
|
&& self.field4 != 0
|
||||||
&& key.field5 == VISIBILITYMAP_FORKNUM
|
&& self.field5 == FSM_FORKNUM
|
||||||
&& key.field6 != 0xffffffff
|
&& self.field6 != 0xffffffff
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn key_to_slru_block(key: Key) -> anyhow::Result<(SlruKind, u32, BlockNumber)> {
|
pub fn is_rel_vm_block_key(self) -> bool {
|
||||||
Ok(match key.field1 {
|
self.field1 == 0x00
|
||||||
0x01 => {
|
&& self.field4 != 0
|
||||||
let kind = match key.field2 {
|
&& self.field5 == VISIBILITYMAP_FORKNUM
|
||||||
0x00 => SlruKind::Clog,
|
&& self.field6 != 0xffffffff
|
||||||
0x01 => SlruKind::MultiXactMembers,
|
}
|
||||||
0x02 => SlruKind::MultiXactOffsets,
|
|
||||||
_ => anyhow::bail!("unrecognized slru kind 0x{:02x}", key.field2),
|
|
||||||
};
|
|
||||||
let segno = key.field4;
|
|
||||||
let blknum = key.field6;
|
|
||||||
|
|
||||||
(kind, segno, blknum)
|
#[inline(always)]
|
||||||
}
|
pub fn to_slru_block(self) -> anyhow::Result<(SlruKind, u32, BlockNumber)> {
|
||||||
_ => anyhow::bail!("unexpected value kind 0x{:02x}", key.field1),
|
Ok(match self.field1 {
|
||||||
})
|
0x01 => {
|
||||||
}
|
let kind = match self.field2 {
|
||||||
|
0x00 => SlruKind::Clog,
|
||||||
|
0x01 => SlruKind::MultiXactMembers,
|
||||||
|
0x02 => SlruKind::MultiXactOffsets,
|
||||||
|
_ => anyhow::bail!("unrecognized slru kind 0x{:02x}", self.field2),
|
||||||
|
};
|
||||||
|
let segno = self.field4;
|
||||||
|
let blknum = self.field6;
|
||||||
|
|
||||||
#[inline(always)]
|
(kind, segno, blknum)
|
||||||
pub fn is_slru_block_key(key: Key) -> bool {
|
}
|
||||||
key.field1 == 0x01 // SLRU-related
|
_ => anyhow::bail!("unexpected value kind 0x{:02x}", self.field1),
|
||||||
&& key.field3 == 0x00000001 // but not SlruDir
|
})
|
||||||
&& key.field6 != 0xffffffff // and not SlruSegSize
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn is_rel_block_key(key: &Key) -> bool {
|
pub fn is_slru_block_key(self) -> bool {
|
||||||
key.field1 == 0x00 && key.field4 != 0 && key.field6 != 0xffffffff
|
self.field1 == 0x01 // SLRU-related
|
||||||
}
|
&& self.field3 == 0x00000001 // but not SlruDir
|
||||||
|
&& self.field6 != 0xffffffff // and not SlruSegSize
|
||||||
|
}
|
||||||
|
|
||||||
/// Guaranteed to return `Ok()` if [[is_rel_block_key]] returns `true` for `key`.
|
#[inline(always)]
|
||||||
#[inline(always)]
|
pub fn is_rel_block_key(&self) -> bool {
|
||||||
pub fn key_to_rel_block(key: Key) -> anyhow::Result<(RelTag, BlockNumber)> {
|
self.field1 == 0x00 && self.field4 != 0 && self.field6 != 0xffffffff
|
||||||
Ok(match key.field1 {
|
}
|
||||||
0x00 => (
|
|
||||||
RelTag {
|
/// Guaranteed to return `Ok()` if [`Self::is_rel_block_key`] returns `true` for `key`.
|
||||||
spcnode: key.field2,
|
#[inline(always)]
|
||||||
dbnode: key.field3,
|
pub fn to_rel_block(self) -> anyhow::Result<(RelTag, BlockNumber)> {
|
||||||
relnode: key.field4,
|
Ok(match self.field1 {
|
||||||
forknum: key.field5,
|
0x00 => (
|
||||||
},
|
RelTag {
|
||||||
key.field6,
|
spcnode: self.field2,
|
||||||
),
|
dbnode: self.field3,
|
||||||
_ => anyhow::bail!("unexpected value kind 0x{:02x}", key.field1),
|
relnode: self.field4,
|
||||||
})
|
forknum: self.field5,
|
||||||
|
},
|
||||||
|
self.field6,
|
||||||
|
),
|
||||||
|
_ => anyhow::bail!("unexpected value kind 0x{:02x}", self.field1),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for Key {
|
impl std::str::FromStr for Key {
|
||||||
@@ -556,11 +717,14 @@ impl std::str::FromStr for Key {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use crate::key::is_metadata_key_slice;
|
||||||
use crate::key::Key;
|
use crate::key::Key;
|
||||||
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
|
|
||||||
|
use super::AUX_KEY_PREFIX;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn display_fromstr_bijection() {
|
fn display_fromstr_bijection() {
|
||||||
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
let mut rng = rand::rngs::StdRng::seed_from_u64(42);
|
||||||
@@ -576,4 +740,21 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(key, Key::from_str(&format!("{key}")).unwrap());
|
assert_eq!(key, Key::from_str(&format!("{key}")).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_metadata_keys() {
|
||||||
|
let mut metadata_key = vec![AUX_KEY_PREFIX];
|
||||||
|
metadata_key.extend_from_slice(&[0xFF; 15]);
|
||||||
|
let encoded_key = Key::from_metadata_key(&metadata_key);
|
||||||
|
let output_key = encoded_key.to_i128().to_be_bytes();
|
||||||
|
assert_eq!(metadata_key, output_key);
|
||||||
|
assert!(encoded_key.is_metadata_key());
|
||||||
|
assert!(is_metadata_key_slice(&metadata_key));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_possible_largest_key() {
|
||||||
|
Key::from_i128(0x7FFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF_FFFF);
|
||||||
|
// TODO: put this key into the system and see if anything breaks.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
#![deny(clippy::undocumented_unsafe_blocks)]
|
#![deny(clippy::undocumented_unsafe_blocks)]
|
||||||
use const_format::formatcp;
|
|
||||||
|
|
||||||
pub mod controller_api;
|
pub mod controller_api;
|
||||||
pub mod key;
|
pub mod key;
|
||||||
@@ -11,7 +10,4 @@ pub mod shard;
|
|||||||
/// Public API types
|
/// Public API types
|
||||||
pub mod upcall_api;
|
pub mod upcall_api;
|
||||||
|
|
||||||
pub const DEFAULT_PG_LISTEN_PORT: u16 = 64000;
|
pub mod config;
|
||||||
pub const DEFAULT_PG_LISTEN_ADDR: &str = formatcp!("127.0.0.1:{DEFAULT_PG_LISTEN_PORT}");
|
|
||||||
pub const DEFAULT_HTTP_LISTEN_PORT: u16 = 9898;
|
|
||||||
pub const DEFAULT_HTTP_LISTEN_ADDR: &str = formatcp!("127.0.0.1:{DEFAULT_HTTP_LISTEN_PORT}");
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
pub mod detach_ancestor;
|
||||||
pub mod partitioning;
|
pub mod partitioning;
|
||||||
pub mod utilization;
|
pub mod utilization;
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ use std::{
|
|||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
io::{BufRead, Read},
|
io::{BufRead, Read},
|
||||||
num::{NonZeroU64, NonZeroUsize},
|
num::{NonZeroU64, NonZeroUsize},
|
||||||
|
sync::atomic::AtomicUsize,
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -20,6 +22,7 @@ use utils::{
|
|||||||
history_buffer::HistoryBufferWithDropCounter,
|
history_buffer::HistoryBufferWithDropCounter,
|
||||||
id::{NodeId, TenantId, TimelineId},
|
id::{NodeId, TenantId, TimelineId},
|
||||||
lsn::Lsn,
|
lsn::Lsn,
|
||||||
|
serde_system_time,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::controller_api::PlacementPolicy;
|
use crate::controller_api::PlacementPolicy;
|
||||||
@@ -158,6 +161,36 @@ impl std::fmt::Debug for TenantState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A temporary lease to a specific lsn inside a timeline.
|
||||||
|
/// Access to the lsn is guaranteed by the pageserver until the expiration indicated by `valid_until`.
|
||||||
|
#[serde_as]
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct LsnLease {
|
||||||
|
#[serde_as(as = "SystemTimeAsRfc3339Millis")]
|
||||||
|
pub valid_until: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
serde_with::serde_conv!(
|
||||||
|
SystemTimeAsRfc3339Millis,
|
||||||
|
SystemTime,
|
||||||
|
|time: &SystemTime| humantime::format_rfc3339_millis(*time).to_string(),
|
||||||
|
|value: String| -> Result<_, humantime::TimestampError> { humantime::parse_rfc3339(&value) }
|
||||||
|
);
|
||||||
|
|
||||||
|
impl LsnLease {
|
||||||
|
/// The default length for an explicit LSN lease request (10 minutes).
|
||||||
|
pub const DEFAULT_LENGTH: Duration = Duration::from_secs(10 * 60);
|
||||||
|
|
||||||
|
/// The default length for an implicit LSN lease granted during
|
||||||
|
/// `get_lsn_by_timestamp` request (1 minutes).
|
||||||
|
pub const DEFAULT_LENGTH_FOR_TS: Duration = Duration::from_secs(60);
|
||||||
|
|
||||||
|
/// Checks whether the lease is expired.
|
||||||
|
pub fn is_expired(&self, now: &SystemTime) -> bool {
|
||||||
|
now > &self.valid_until
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The only [`TenantState`] variants we could be `TenantState::Activating` from.
|
/// The only [`TenantState`] variants we could be `TenantState::Activating` from.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum ActivatingFrom {
|
pub enum ActivatingFrom {
|
||||||
@@ -260,22 +293,6 @@ pub struct TenantCreateRequest {
|
|||||||
pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it
|
pub config: TenantConfig, // as we have a flattened field, we should reject all unknown fields in it
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct TenantLoadRequest {
|
|
||||||
#[serde(default)]
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub generation: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for TenantCreateRequest {
|
|
||||||
type Target = TenantConfig;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// An alternative representation of `pageserver::tenant::TenantConf` with
|
/// An alternative representation of `pageserver::tenant::TenantConf` with
|
||||||
/// simpler types.
|
/// simpler types.
|
||||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
|
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
|
||||||
@@ -286,7 +303,7 @@ pub struct TenantConfig {
|
|||||||
pub compaction_period: Option<String>,
|
pub compaction_period: Option<String>,
|
||||||
pub compaction_threshold: Option<usize>,
|
pub compaction_threshold: Option<usize>,
|
||||||
// defer parsing compaction_algorithm, like eviction_policy
|
// defer parsing compaction_algorithm, like eviction_policy
|
||||||
pub compaction_algorithm: Option<CompactionAlgorithm>,
|
pub compaction_algorithm: Option<CompactionAlgorithmSettings>,
|
||||||
pub gc_horizon: Option<u64>,
|
pub gc_horizon: Option<u64>,
|
||||||
pub gc_period: Option<String>,
|
pub gc_period: Option<String>,
|
||||||
pub image_creation_threshold: Option<usize>,
|
pub image_creation_threshold: Option<usize>,
|
||||||
@@ -301,6 +318,106 @@ pub struct TenantConfig {
|
|||||||
pub heatmap_period: Option<String>,
|
pub heatmap_period: Option<String>,
|
||||||
pub lazy_slru_download: Option<bool>,
|
pub lazy_slru_download: Option<bool>,
|
||||||
pub timeline_get_throttle: Option<ThrottleConfig>,
|
pub timeline_get_throttle: Option<ThrottleConfig>,
|
||||||
|
pub image_layer_creation_check_threshold: Option<u8>,
|
||||||
|
pub switch_aux_file_policy: Option<AuxFilePolicy>,
|
||||||
|
pub lsn_lease_length: Option<String>,
|
||||||
|
pub lsn_lease_length_for_ts: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The policy for the aux file storage. It can be switched through `switch_aux_file_policy`
|
||||||
|
/// tenant config. When the first aux file written, the policy will be persisted in the
|
||||||
|
/// `index_part.json` file and has a limited migration path.
|
||||||
|
///
|
||||||
|
/// Currently, we only allow the following migration path:
|
||||||
|
///
|
||||||
|
/// Unset -> V1
|
||||||
|
/// -> V2
|
||||||
|
/// -> CrossValidation -> V2
|
||||||
|
#[derive(
|
||||||
|
Eq,
|
||||||
|
PartialEq,
|
||||||
|
Debug,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
strum_macros::EnumString,
|
||||||
|
strum_macros::Display,
|
||||||
|
serde_with::DeserializeFromStr,
|
||||||
|
serde_with::SerializeDisplay,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
pub enum AuxFilePolicy {
|
||||||
|
/// V1 aux file policy: store everything in AUX_FILE_KEY
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
V1,
|
||||||
|
/// V2 aux file policy: store in the AUX_FILE keyspace
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
V2,
|
||||||
|
/// Cross validation runs both formats on the write path and does validation
|
||||||
|
/// on the read path.
|
||||||
|
#[strum(ascii_case_insensitive)]
|
||||||
|
CrossValidation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuxFilePolicy {
|
||||||
|
pub fn is_valid_migration_path(from: Option<Self>, to: Self) -> bool {
|
||||||
|
matches!(
|
||||||
|
(from, to),
|
||||||
|
(None, _) | (Some(AuxFilePolicy::CrossValidation), AuxFilePolicy::V2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If a tenant writes aux files without setting `switch_aux_policy`, this value will be used.
|
||||||
|
pub fn default_tenant_config() -> Self {
|
||||||
|
Self::V1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The aux file policy memory flag. Users can store `Option<AuxFilePolicy>` into this atomic flag. 0 == unspecified.
|
||||||
|
pub struct AtomicAuxFilePolicy(AtomicUsize);
|
||||||
|
|
||||||
|
impl AtomicAuxFilePolicy {
|
||||||
|
pub fn new(policy: Option<AuxFilePolicy>) -> Self {
|
||||||
|
Self(AtomicUsize::new(
|
||||||
|
policy.map(AuxFilePolicy::to_usize).unwrap_or_default(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(&self) -> Option<AuxFilePolicy> {
|
||||||
|
match self.0.load(std::sync::atomic::Ordering::Acquire) {
|
||||||
|
0 => None,
|
||||||
|
other => Some(AuxFilePolicy::from_usize(other)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store(&self, policy: Option<AuxFilePolicy>) {
|
||||||
|
self.0.store(
|
||||||
|
policy.map(AuxFilePolicy::to_usize).unwrap_or_default(),
|
||||||
|
std::sync::atomic::Ordering::Release,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AuxFilePolicy {
|
||||||
|
pub fn to_usize(self) -> usize {
|
||||||
|
match self {
|
||||||
|
Self::V1 => 1,
|
||||||
|
Self::CrossValidation => 2,
|
||||||
|
Self::V2 => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_from_usize(this: usize) -> Option<Self> {
|
||||||
|
match this {
|
||||||
|
1 => Some(Self::V1),
|
||||||
|
2 => Some(Self::CrossValidation),
|
||||||
|
3 => Some(Self::V2),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_usize(this: usize) -> Self {
|
||||||
|
Self::try_from_usize(this).unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
@@ -321,13 +438,28 @@ impl EvictionPolicy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(
|
||||||
#[serde(tag = "kind")]
|
Eq,
|
||||||
|
PartialEq,
|
||||||
|
Debug,
|
||||||
|
Copy,
|
||||||
|
Clone,
|
||||||
|
strum_macros::EnumString,
|
||||||
|
strum_macros::Display,
|
||||||
|
serde_with::DeserializeFromStr,
|
||||||
|
serde_with::SerializeDisplay,
|
||||||
|
)]
|
||||||
|
#[strum(serialize_all = "kebab-case")]
|
||||||
pub enum CompactionAlgorithm {
|
pub enum CompactionAlgorithm {
|
||||||
Legacy,
|
Legacy,
|
||||||
Tiered,
|
Tiered,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Eq, PartialEq, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CompactionAlgorithmSettings {
|
||||||
|
pub kind: CompactionAlgorithm,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct EvictionPolicyLayerAccessThreshold {
|
pub struct EvictionPolicyLayerAccessThreshold {
|
||||||
#[serde(with = "humantime_serde")]
|
#[serde(with = "humantime_serde")]
|
||||||
@@ -427,7 +559,6 @@ pub struct StatusResponse {
|
|||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct TenantLocationConfigRequest {
|
pub struct TenantLocationConfigRequest {
|
||||||
pub tenant_id: Option<TenantShardId>,
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub config: LocationConfig, // as we have a flattened field, we should reject all unknown fields in it
|
pub config: LocationConfig, // as we have a flattened field, we should reject all unknown fields in it
|
||||||
}
|
}
|
||||||
@@ -476,31 +607,6 @@ impl TenantConfigRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct TenantAttachRequest {
|
|
||||||
#[serde(default)]
|
|
||||||
pub config: TenantAttachConfig,
|
|
||||||
#[serde(default)]
|
|
||||||
pub generation: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Newtype to enforce deny_unknown_fields on TenantConfig for
|
|
||||||
/// its usage inside `TenantAttachRequest`.
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Default)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
pub struct TenantAttachConfig {
|
|
||||||
#[serde(flatten)]
|
|
||||||
allowing_unknown_fields: TenantConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for TenantAttachConfig {
|
|
||||||
type Target = TenantConfig;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.allowing_unknown_fields
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See [`TenantState::attachment_status`] and the OpenAPI docs for context.
|
/// See [`TenantState::attachment_status`] and the OpenAPI docs for context.
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
#[serde(tag = "slug", content = "data", rename_all = "snake_case")]
|
#[serde(tag = "slug", content = "data", rename_all = "snake_case")]
|
||||||
@@ -519,8 +625,7 @@ pub struct TenantInfo {
|
|||||||
/// If a layer is present in both local FS and S3, it counts only once.
|
/// If a layer is present in both local FS and S3, it counts only once.
|
||||||
pub current_physical_size: Option<u64>, // physical size is only included in `tenant_status` endpoint
|
pub current_physical_size: Option<u64>, // physical size is only included in `tenant_status` endpoint
|
||||||
pub attachment_status: TenantAttachmentStatus,
|
pub attachment_status: TenantAttachmentStatus,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
pub generation: u32,
|
||||||
pub generation: Option<u32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
@@ -576,6 +681,9 @@ pub struct TimelineInfo {
|
|||||||
pub state: TimelineState,
|
pub state: TimelineState,
|
||||||
|
|
||||||
pub walreceiver_status: String,
|
pub walreceiver_status: String,
|
||||||
|
|
||||||
|
/// The last aux file policy being used on this timeline
|
||||||
|
pub last_aux_file_policy: Option<AuxFilePolicy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
@@ -682,6 +790,8 @@ pub enum HistoricLayerInfo {
|
|||||||
lsn_end: Lsn,
|
lsn_end: Lsn,
|
||||||
remote: bool,
|
remote: bool,
|
||||||
access_stats: LayerAccessStats,
|
access_stats: LayerAccessStats,
|
||||||
|
|
||||||
|
l0: bool,
|
||||||
},
|
},
|
||||||
Image {
|
Image {
|
||||||
layer_file_name: String,
|
layer_file_name: String,
|
||||||
@@ -717,6 +827,16 @@ impl HistoricLayerInfo {
|
|||||||
};
|
};
|
||||||
*field = value;
|
*field = value;
|
||||||
}
|
}
|
||||||
|
pub fn layer_file_size(&self) -> u64 {
|
||||||
|
match self {
|
||||||
|
HistoricLayerInfo::Delta {
|
||||||
|
layer_file_size, ..
|
||||||
|
} => *layer_file_size,
|
||||||
|
HistoricLayerInfo::Image {
|
||||||
|
layer_file_size, ..
|
||||||
|
} => *layer_file_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -724,6 +844,16 @@ pub struct DownloadRemoteLayersTaskSpawnRequest {
|
|||||||
pub max_concurrent_downloads: NonZeroUsize,
|
pub max_concurrent_downloads: NonZeroUsize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct IngestAuxFilesRequest {
|
||||||
|
pub aux_files: HashMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct ListAuxFilesRequest {
|
||||||
|
pub lsn: Lsn,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct DownloadRemoteLayersTaskInfo {
|
pub struct DownloadRemoteLayersTaskInfo {
|
||||||
pub task_id: String,
|
pub task_id: String,
|
||||||
@@ -745,10 +875,15 @@ pub struct TimelineGcRequest {
|
|||||||
pub gc_horizon: Option<u64>,
|
pub gc_horizon: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WalRedoManagerProcessStatus {
|
||||||
|
pub pid: u32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WalRedoManagerStatus {
|
pub struct WalRedoManagerStatus {
|
||||||
pub last_redo_at: Option<chrono::DateTime<chrono::Utc>>,
|
pub last_redo_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
pub pid: Option<u32>,
|
pub process: Option<WalRedoManagerProcessStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The progress of a secondary tenant is mostly useful when doing a long running download: e.g. initiating
|
/// The progress of a secondary tenant is mostly useful when doing a long running download: e.g. initiating
|
||||||
@@ -757,11 +892,7 @@ pub struct WalRedoManagerStatus {
|
|||||||
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
#[derive(Default, Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct SecondaryProgress {
|
pub struct SecondaryProgress {
|
||||||
/// The remote storage LastModified time of the heatmap object we last downloaded.
|
/// The remote storage LastModified time of the heatmap object we last downloaded.
|
||||||
#[serde(
|
pub heatmap_mtime: Option<serde_system_time::SystemTime>,
|
||||||
serialize_with = "opt_ser_rfc3339_millis",
|
|
||||||
deserialize_with = "opt_deser_rfc3339_millis"
|
|
||||||
)]
|
|
||||||
pub heatmap_mtime: Option<SystemTime>,
|
|
||||||
|
|
||||||
/// The number of layers currently on-disk
|
/// The number of layers currently on-disk
|
||||||
pub layers_downloaded: usize,
|
pub layers_downloaded: usize,
|
||||||
@@ -774,27 +905,64 @@ pub struct SecondaryProgress {
|
|||||||
pub bytes_total: u64,
|
pub bytes_total: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opt_ser_rfc3339_millis<S: serde::Serializer>(
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
ts: &Option<SystemTime>,
|
pub struct TenantScanRemoteStorageShard {
|
||||||
serializer: S,
|
pub tenant_shard_id: TenantShardId,
|
||||||
) -> Result<S::Ok, S::Error> {
|
pub generation: Option<u32>,
|
||||||
match ts {
|
}
|
||||||
Some(ts) => serializer.collect_str(&humantime::format_rfc3339_millis(*ts)),
|
|
||||||
None => serializer.serialize_none(),
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
pub struct TenantScanRemoteStorageResponse {
|
||||||
|
pub shards: Vec<TenantScanRemoteStorageShard>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum TenantSorting {
|
||||||
|
ResidentSize,
|
||||||
|
MaxLogicalSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TenantSorting {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::ResidentSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opt_deser_rfc3339_millis<'de, D>(deserializer: D) -> Result<Option<SystemTime>, D::Error>
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
where
|
pub struct TopTenantShardsRequest {
|
||||||
D: serde::de::Deserializer<'de>,
|
// How would you like to sort the tenants?
|
||||||
{
|
pub order_by: TenantSorting,
|
||||||
let s: Option<String> = serde::de::Deserialize::deserialize(deserializer)?;
|
|
||||||
match s {
|
// How many results?
|
||||||
None => Ok(None),
|
pub limit: usize,
|
||||||
Some(s) => humantime::parse_rfc3339(&s)
|
|
||||||
.map_err(serde::de::Error::custom)
|
// Omit tenants with more than this many shards (e.g. if this is the max number of shards
|
||||||
.map(Some),
|
// that the caller would ever split to)
|
||||||
}
|
pub where_shards_lt: Option<ShardCount>,
|
||||||
|
|
||||||
|
// Omit tenants where the ordering metric is less than this (this is an optimization to
|
||||||
|
// let us quickly exclude numerous tiny shards)
|
||||||
|
pub where_gt: Option<u64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
|
||||||
|
pub struct TopTenantShardItem {
|
||||||
|
pub id: TenantShardId,
|
||||||
|
|
||||||
|
/// Total size of layers on local disk for all timelines in this tenant
|
||||||
|
pub resident_size: u64,
|
||||||
|
|
||||||
|
/// Total size of layers in remote storage for all timelines in this tenant
|
||||||
|
pub physical_size: u64,
|
||||||
|
|
||||||
|
/// The largest logical size of a timeline within this tenant
|
||||||
|
pub max_logical_size: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Default)]
|
||||||
|
pub struct TopTenantShardsResponse {
|
||||||
|
pub shards: Vec<TopTenantShardItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod virtual_file {
|
pub mod virtual_file {
|
||||||
@@ -864,39 +1032,72 @@ impl TryFrom<u8> for PagestreamBeMessageTag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In the V2 protocol version, a GetPage request contains two LSN values:
|
||||||
|
//
|
||||||
|
// request_lsn: Get the page version at this point in time. Lsn::Max is a special value that means
|
||||||
|
// "get the latest version present". It's used by the primary server, which knows that no one else
|
||||||
|
// is writing WAL. 'not_modified_since' must be set to a proper value even if request_lsn is
|
||||||
|
// Lsn::Max. Standby servers use the current replay LSN as the request LSN.
|
||||||
|
//
|
||||||
|
// not_modified_since: Hint to the pageserver that the client knows that the page has not been
|
||||||
|
// modified between 'not_modified_since' and the request LSN. It's always correct to set
|
||||||
|
// 'not_modified_since equal' to 'request_lsn' (unless Lsn::Max is used as the 'request_lsn'), but
|
||||||
|
// passing an earlier LSN can speed up the request, by allowing the pageserver to process the
|
||||||
|
// request without waiting for 'request_lsn' to arrive.
|
||||||
|
//
|
||||||
|
// The legacy V1 interface contained only one LSN, and a boolean 'latest' flag. The V1 interface was
|
||||||
|
// sufficient for the primary; the 'lsn' was equivalent to the 'not_modified_since' value, and
|
||||||
|
// 'latest' was set to true. The V2 interface was added because there was no correct way for a
|
||||||
|
// standby to request a page at a particular non-latest LSN, and also include the
|
||||||
|
// 'not_modified_since' hint. That led to an awkward choice of either using an old LSN in the
|
||||||
|
// request, if the standby knows that the page hasn't been modified since, and risk getting an error
|
||||||
|
// if that LSN has fallen behind the GC horizon, or requesting the current replay LSN, which could
|
||||||
|
// require the pageserver unnecessarily to wait for the WAL to arrive up to that point. The new V2
|
||||||
|
// interface allows sending both LSNs, and let the pageserver do the right thing. There is no
|
||||||
|
// difference in the responses between V1 and V2.
|
||||||
|
//
|
||||||
|
// The Request structs below reflect the V2 interface. If V1 is used, the parse function
|
||||||
|
// maps the old format requests to the new format.
|
||||||
|
//
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum PagestreamProtocolVersion {
|
||||||
|
V1,
|
||||||
|
V2,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct PagestreamExistsRequest {
|
pub struct PagestreamExistsRequest {
|
||||||
pub latest: bool,
|
pub request_lsn: Lsn,
|
||||||
pub lsn: Lsn,
|
pub not_modified_since: Lsn,
|
||||||
pub rel: RelTag,
|
pub rel: RelTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct PagestreamNblocksRequest {
|
pub struct PagestreamNblocksRequest {
|
||||||
pub latest: bool,
|
pub request_lsn: Lsn,
|
||||||
pub lsn: Lsn,
|
pub not_modified_since: Lsn,
|
||||||
pub rel: RelTag,
|
pub rel: RelTag,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct PagestreamGetPageRequest {
|
pub struct PagestreamGetPageRequest {
|
||||||
pub latest: bool,
|
pub request_lsn: Lsn,
|
||||||
pub lsn: Lsn,
|
pub not_modified_since: Lsn,
|
||||||
pub rel: RelTag,
|
pub rel: RelTag,
|
||||||
pub blkno: u32,
|
pub blkno: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct PagestreamDbSizeRequest {
|
pub struct PagestreamDbSizeRequest {
|
||||||
pub latest: bool,
|
pub request_lsn: Lsn,
|
||||||
pub lsn: Lsn,
|
pub not_modified_since: Lsn,
|
||||||
pub dbnode: u32,
|
pub dbnode: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct PagestreamGetSlruSegmentRequest {
|
pub struct PagestreamGetSlruSegmentRequest {
|
||||||
pub latest: bool,
|
pub request_lsn: Lsn,
|
||||||
pub lsn: Lsn,
|
pub not_modified_since: Lsn,
|
||||||
pub kind: u8,
|
pub kind: u8,
|
||||||
pub segno: u32,
|
pub segno: u32,
|
||||||
}
|
}
|
||||||
@@ -943,14 +1144,16 @@ pub struct TenantHistorySize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PagestreamFeMessage {
|
impl PagestreamFeMessage {
|
||||||
|
/// Serialize a compute -> pageserver message. This is currently only used in testing
|
||||||
|
/// tools. Always uses protocol version 2.
|
||||||
pub fn serialize(&self) -> Bytes {
|
pub fn serialize(&self) -> Bytes {
|
||||||
let mut bytes = BytesMut::new();
|
let mut bytes = BytesMut::new();
|
||||||
|
|
||||||
match self {
|
match self {
|
||||||
Self::Exists(req) => {
|
Self::Exists(req) => {
|
||||||
bytes.put_u8(0);
|
bytes.put_u8(0);
|
||||||
bytes.put_u8(u8::from(req.latest));
|
bytes.put_u64(req.request_lsn.0);
|
||||||
bytes.put_u64(req.lsn.0);
|
bytes.put_u64(req.not_modified_since.0);
|
||||||
bytes.put_u32(req.rel.spcnode);
|
bytes.put_u32(req.rel.spcnode);
|
||||||
bytes.put_u32(req.rel.dbnode);
|
bytes.put_u32(req.rel.dbnode);
|
||||||
bytes.put_u32(req.rel.relnode);
|
bytes.put_u32(req.rel.relnode);
|
||||||
@@ -959,8 +1162,8 @@ impl PagestreamFeMessage {
|
|||||||
|
|
||||||
Self::Nblocks(req) => {
|
Self::Nblocks(req) => {
|
||||||
bytes.put_u8(1);
|
bytes.put_u8(1);
|
||||||
bytes.put_u8(u8::from(req.latest));
|
bytes.put_u64(req.request_lsn.0);
|
||||||
bytes.put_u64(req.lsn.0);
|
bytes.put_u64(req.not_modified_since.0);
|
||||||
bytes.put_u32(req.rel.spcnode);
|
bytes.put_u32(req.rel.spcnode);
|
||||||
bytes.put_u32(req.rel.dbnode);
|
bytes.put_u32(req.rel.dbnode);
|
||||||
bytes.put_u32(req.rel.relnode);
|
bytes.put_u32(req.rel.relnode);
|
||||||
@@ -969,8 +1172,8 @@ impl PagestreamFeMessage {
|
|||||||
|
|
||||||
Self::GetPage(req) => {
|
Self::GetPage(req) => {
|
||||||
bytes.put_u8(2);
|
bytes.put_u8(2);
|
||||||
bytes.put_u8(u8::from(req.latest));
|
bytes.put_u64(req.request_lsn.0);
|
||||||
bytes.put_u64(req.lsn.0);
|
bytes.put_u64(req.not_modified_since.0);
|
||||||
bytes.put_u32(req.rel.spcnode);
|
bytes.put_u32(req.rel.spcnode);
|
||||||
bytes.put_u32(req.rel.dbnode);
|
bytes.put_u32(req.rel.dbnode);
|
||||||
bytes.put_u32(req.rel.relnode);
|
bytes.put_u32(req.rel.relnode);
|
||||||
@@ -980,15 +1183,15 @@ impl PagestreamFeMessage {
|
|||||||
|
|
||||||
Self::DbSize(req) => {
|
Self::DbSize(req) => {
|
||||||
bytes.put_u8(3);
|
bytes.put_u8(3);
|
||||||
bytes.put_u8(u8::from(req.latest));
|
bytes.put_u64(req.request_lsn.0);
|
||||||
bytes.put_u64(req.lsn.0);
|
bytes.put_u64(req.not_modified_since.0);
|
||||||
bytes.put_u32(req.dbnode);
|
bytes.put_u32(req.dbnode);
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::GetSlruSegment(req) => {
|
Self::GetSlruSegment(req) => {
|
||||||
bytes.put_u8(4);
|
bytes.put_u8(4);
|
||||||
bytes.put_u8(u8::from(req.latest));
|
bytes.put_u64(req.request_lsn.0);
|
||||||
bytes.put_u64(req.lsn.0);
|
bytes.put_u64(req.not_modified_since.0);
|
||||||
bytes.put_u8(req.kind);
|
bytes.put_u8(req.kind);
|
||||||
bytes.put_u32(req.segno);
|
bytes.put_u32(req.segno);
|
||||||
}
|
}
|
||||||
@@ -997,18 +1200,40 @@ impl PagestreamFeMessage {
|
|||||||
bytes.into()
|
bytes.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse<R: std::io::Read>(body: &mut R) -> anyhow::Result<PagestreamFeMessage> {
|
pub fn parse<R: std::io::Read>(
|
||||||
// TODO these gets can fail
|
body: &mut R,
|
||||||
|
protocol_version: PagestreamProtocolVersion,
|
||||||
|
) -> anyhow::Result<PagestreamFeMessage> {
|
||||||
// these correspond to the NeonMessageTag enum in pagestore_client.h
|
// these correspond to the NeonMessageTag enum in pagestore_client.h
|
||||||
//
|
//
|
||||||
// TODO: consider using protobuf or serde bincode for less error prone
|
// TODO: consider using protobuf or serde bincode for less error prone
|
||||||
// serialization.
|
// serialization.
|
||||||
let msg_tag = body.read_u8()?;
|
let msg_tag = body.read_u8()?;
|
||||||
|
|
||||||
|
let (request_lsn, not_modified_since) = match protocol_version {
|
||||||
|
PagestreamProtocolVersion::V2 => (
|
||||||
|
Lsn::from(body.read_u64::<BigEndian>()?),
|
||||||
|
Lsn::from(body.read_u64::<BigEndian>()?),
|
||||||
|
),
|
||||||
|
PagestreamProtocolVersion::V1 => {
|
||||||
|
// In the old protocol, each message starts with a boolean 'latest' flag,
|
||||||
|
// followed by 'lsn'. Convert that to the two LSNs, 'request_lsn' and
|
||||||
|
// 'not_modified_since', used in the new protocol version.
|
||||||
|
let latest = body.read_u8()? != 0;
|
||||||
|
let request_lsn = Lsn::from(body.read_u64::<BigEndian>()?);
|
||||||
|
if latest {
|
||||||
|
(Lsn::MAX, request_lsn) // get latest version
|
||||||
|
} else {
|
||||||
|
(request_lsn, request_lsn) // get version at specified LSN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The rest of the messages are the same between V1 and V2
|
||||||
match msg_tag {
|
match msg_tag {
|
||||||
0 => Ok(PagestreamFeMessage::Exists(PagestreamExistsRequest {
|
0 => Ok(PagestreamFeMessage::Exists(PagestreamExistsRequest {
|
||||||
latest: body.read_u8()? != 0,
|
request_lsn,
|
||||||
lsn: Lsn::from(body.read_u64::<BigEndian>()?),
|
not_modified_since,
|
||||||
rel: RelTag {
|
rel: RelTag {
|
||||||
spcnode: body.read_u32::<BigEndian>()?,
|
spcnode: body.read_u32::<BigEndian>()?,
|
||||||
dbnode: body.read_u32::<BigEndian>()?,
|
dbnode: body.read_u32::<BigEndian>()?,
|
||||||
@@ -1017,8 +1242,8 @@ impl PagestreamFeMessage {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
1 => Ok(PagestreamFeMessage::Nblocks(PagestreamNblocksRequest {
|
1 => Ok(PagestreamFeMessage::Nblocks(PagestreamNblocksRequest {
|
||||||
latest: body.read_u8()? != 0,
|
request_lsn,
|
||||||
lsn: Lsn::from(body.read_u64::<BigEndian>()?),
|
not_modified_since,
|
||||||
rel: RelTag {
|
rel: RelTag {
|
||||||
spcnode: body.read_u32::<BigEndian>()?,
|
spcnode: body.read_u32::<BigEndian>()?,
|
||||||
dbnode: body.read_u32::<BigEndian>()?,
|
dbnode: body.read_u32::<BigEndian>()?,
|
||||||
@@ -1027,8 +1252,8 @@ impl PagestreamFeMessage {
|
|||||||
},
|
},
|
||||||
})),
|
})),
|
||||||
2 => Ok(PagestreamFeMessage::GetPage(PagestreamGetPageRequest {
|
2 => Ok(PagestreamFeMessage::GetPage(PagestreamGetPageRequest {
|
||||||
latest: body.read_u8()? != 0,
|
request_lsn,
|
||||||
lsn: Lsn::from(body.read_u64::<BigEndian>()?),
|
not_modified_since,
|
||||||
rel: RelTag {
|
rel: RelTag {
|
||||||
spcnode: body.read_u32::<BigEndian>()?,
|
spcnode: body.read_u32::<BigEndian>()?,
|
||||||
dbnode: body.read_u32::<BigEndian>()?,
|
dbnode: body.read_u32::<BigEndian>()?,
|
||||||
@@ -1038,14 +1263,14 @@ impl PagestreamFeMessage {
|
|||||||
blkno: body.read_u32::<BigEndian>()?,
|
blkno: body.read_u32::<BigEndian>()?,
|
||||||
})),
|
})),
|
||||||
3 => Ok(PagestreamFeMessage::DbSize(PagestreamDbSizeRequest {
|
3 => Ok(PagestreamFeMessage::DbSize(PagestreamDbSizeRequest {
|
||||||
latest: body.read_u8()? != 0,
|
request_lsn,
|
||||||
lsn: Lsn::from(body.read_u64::<BigEndian>()?),
|
not_modified_since,
|
||||||
dbnode: body.read_u32::<BigEndian>()?,
|
dbnode: body.read_u32::<BigEndian>()?,
|
||||||
})),
|
})),
|
||||||
4 => Ok(PagestreamFeMessage::GetSlruSegment(
|
4 => Ok(PagestreamFeMessage::GetSlruSegment(
|
||||||
PagestreamGetSlruSegmentRequest {
|
PagestreamGetSlruSegmentRequest {
|
||||||
latest: body.read_u8()? != 0,
|
request_lsn,
|
||||||
lsn: Lsn::from(body.read_u64::<BigEndian>()?),
|
not_modified_since,
|
||||||
kind: body.read_u8()?,
|
kind: body.read_u8()?,
|
||||||
segno: body.read_u32::<BigEndian>()?,
|
segno: body.read_u32::<BigEndian>()?,
|
||||||
},
|
},
|
||||||
@@ -1165,6 +1390,7 @@ impl PagestreamBeMessage {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -1173,8 +1399,8 @@ mod tests {
|
|||||||
// Test serialization/deserialization of PagestreamFeMessage
|
// Test serialization/deserialization of PagestreamFeMessage
|
||||||
let messages = vec![
|
let messages = vec![
|
||||||
PagestreamFeMessage::Exists(PagestreamExistsRequest {
|
PagestreamFeMessage::Exists(PagestreamExistsRequest {
|
||||||
latest: true,
|
request_lsn: Lsn(4),
|
||||||
lsn: Lsn(4),
|
not_modified_since: Lsn(3),
|
||||||
rel: RelTag {
|
rel: RelTag {
|
||||||
forknum: 1,
|
forknum: 1,
|
||||||
spcnode: 2,
|
spcnode: 2,
|
||||||
@@ -1183,8 +1409,8 @@ mod tests {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
PagestreamFeMessage::Nblocks(PagestreamNblocksRequest {
|
PagestreamFeMessage::Nblocks(PagestreamNblocksRequest {
|
||||||
latest: false,
|
request_lsn: Lsn(4),
|
||||||
lsn: Lsn(4),
|
not_modified_since: Lsn(4),
|
||||||
rel: RelTag {
|
rel: RelTag {
|
||||||
forknum: 1,
|
forknum: 1,
|
||||||
spcnode: 2,
|
spcnode: 2,
|
||||||
@@ -1193,8 +1419,8 @@ mod tests {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
PagestreamFeMessage::GetPage(PagestreamGetPageRequest {
|
PagestreamFeMessage::GetPage(PagestreamGetPageRequest {
|
||||||
latest: true,
|
request_lsn: Lsn(4),
|
||||||
lsn: Lsn(4),
|
not_modified_since: Lsn(3),
|
||||||
rel: RelTag {
|
rel: RelTag {
|
||||||
forknum: 1,
|
forknum: 1,
|
||||||
spcnode: 2,
|
spcnode: 2,
|
||||||
@@ -1204,14 +1430,16 @@ mod tests {
|
|||||||
blkno: 7,
|
blkno: 7,
|
||||||
}),
|
}),
|
||||||
PagestreamFeMessage::DbSize(PagestreamDbSizeRequest {
|
PagestreamFeMessage::DbSize(PagestreamDbSizeRequest {
|
||||||
latest: true,
|
request_lsn: Lsn(4),
|
||||||
lsn: Lsn(4),
|
not_modified_since: Lsn(3),
|
||||||
dbnode: 7,
|
dbnode: 7,
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
for msg in messages {
|
for msg in messages {
|
||||||
let bytes = msg.serialize();
|
let bytes = msg.serialize();
|
||||||
let reconstructed = PagestreamFeMessage::parse(&mut bytes.reader()).unwrap();
|
let reconstructed =
|
||||||
|
PagestreamFeMessage::parse(&mut bytes.reader(), PagestreamProtocolVersion::V2)
|
||||||
|
.unwrap();
|
||||||
assert!(msg == reconstructed);
|
assert!(msg == reconstructed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1224,7 +1452,7 @@ mod tests {
|
|||||||
state: TenantState::Active,
|
state: TenantState::Active,
|
||||||
current_physical_size: Some(42),
|
current_physical_size: Some(42),
|
||||||
attachment_status: TenantAttachmentStatus::Attached,
|
attachment_status: TenantAttachmentStatus::Attached,
|
||||||
generation: None,
|
generation: 1,
|
||||||
};
|
};
|
||||||
let expected_active = json!({
|
let expected_active = json!({
|
||||||
"id": original_active.id.to_string(),
|
"id": original_active.id.to_string(),
|
||||||
@@ -1234,7 +1462,8 @@ mod tests {
|
|||||||
"current_physical_size": 42,
|
"current_physical_size": 42,
|
||||||
"attachment_status": {
|
"attachment_status": {
|
||||||
"slug":"attached",
|
"slug":"attached",
|
||||||
}
|
},
|
||||||
|
"generation" : 1
|
||||||
});
|
});
|
||||||
|
|
||||||
let original_broken = TenantInfo {
|
let original_broken = TenantInfo {
|
||||||
@@ -1245,7 +1474,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
current_physical_size: Some(42),
|
current_physical_size: Some(42),
|
||||||
attachment_status: TenantAttachmentStatus::Attached,
|
attachment_status: TenantAttachmentStatus::Attached,
|
||||||
generation: None,
|
generation: 1,
|
||||||
};
|
};
|
||||||
let expected_broken = json!({
|
let expected_broken = json!({
|
||||||
"id": original_broken.id.to_string(),
|
"id": original_broken.id.to_string(),
|
||||||
@@ -1259,7 +1488,8 @@ mod tests {
|
|||||||
"current_physical_size": 42,
|
"current_physical_size": 42,
|
||||||
"attachment_status": {
|
"attachment_status": {
|
||||||
"slug":"attached",
|
"slug":"attached",
|
||||||
}
|
},
|
||||||
|
"generation" : 1
|
||||||
});
|
});
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1300,18 +1530,6 @@ mod tests {
|
|||||||
"expect unknown field `unknown_field` error, got: {}",
|
"expect unknown field `unknown_field` error, got: {}",
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
|
|
||||||
let attach_request = json!({
|
|
||||||
"config": {
|
|
||||||
"unknown_field": "unknown_value".to_string(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let err = serde_json::from_value::<TenantAttachRequest>(attach_request).unwrap_err();
|
|
||||||
assert!(
|
|
||||||
err.to_string().contains("unknown field `unknown_field`"),
|
|
||||||
"expect unknown field `unknown_field` error, got: {}",
|
|
||||||
err
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1370,4 +1588,69 @@ mod tests {
|
|||||||
assert_eq!(actual, expected, "example on {line}");
|
assert_eq!(actual, expected, "example on {line}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aux_file_migration_path() {
|
||||||
|
assert!(AuxFilePolicy::is_valid_migration_path(
|
||||||
|
None,
|
||||||
|
AuxFilePolicy::V1
|
||||||
|
));
|
||||||
|
assert!(AuxFilePolicy::is_valid_migration_path(
|
||||||
|
None,
|
||||||
|
AuxFilePolicy::V2
|
||||||
|
));
|
||||||
|
assert!(AuxFilePolicy::is_valid_migration_path(
|
||||||
|
None,
|
||||||
|
AuxFilePolicy::CrossValidation
|
||||||
|
));
|
||||||
|
// Self-migration is not a valid migration path, and the caller should handle it by itself.
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::V1),
|
||||||
|
AuxFilePolicy::V1
|
||||||
|
));
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::V2),
|
||||||
|
AuxFilePolicy::V2
|
||||||
|
));
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::CrossValidation),
|
||||||
|
AuxFilePolicy::CrossValidation
|
||||||
|
));
|
||||||
|
// Migrations not allowed
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::CrossValidation),
|
||||||
|
AuxFilePolicy::V1
|
||||||
|
));
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::V1),
|
||||||
|
AuxFilePolicy::V2
|
||||||
|
));
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::V2),
|
||||||
|
AuxFilePolicy::V1
|
||||||
|
));
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::V2),
|
||||||
|
AuxFilePolicy::CrossValidation
|
||||||
|
));
|
||||||
|
assert!(!AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::V1),
|
||||||
|
AuxFilePolicy::CrossValidation
|
||||||
|
));
|
||||||
|
// Migrations allowed
|
||||||
|
assert!(AuxFilePolicy::is_valid_migration_path(
|
||||||
|
Some(AuxFilePolicy::CrossValidation),
|
||||||
|
AuxFilePolicy::V2
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_aux_parse() {
|
||||||
|
assert_eq!(AuxFilePolicy::from_str("V2").unwrap(), AuxFilePolicy::V2);
|
||||||
|
assert_eq!(AuxFilePolicy::from_str("v2").unwrap(), AuxFilePolicy::V2);
|
||||||
|
assert_eq!(
|
||||||
|
AuxFilePolicy::from_str("cross-validation").unwrap(),
|
||||||
|
AuxFilePolicy::CrossValidation
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
libs/pageserver_api/src/models/detach_ancestor.rs
Normal file
6
libs/pageserver_api/src/models/detach_ancestor.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
use utils::id::TimelineId;
|
||||||
|
|
||||||
|
#[derive(Default, serde::Serialize)]
|
||||||
|
pub struct AncestorDetached {
|
||||||
|
pub reparented_timelines: Vec<TimelineId>,
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
use utils::lsn::Lsn;
|
use utils::lsn::Lsn;
|
||||||
|
|
||||||
|
use crate::keyspace::SparseKeySpace;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub struct Partitioning {
|
pub struct Partitioning {
|
||||||
pub keys: crate::keyspace::KeySpace,
|
pub keys: crate::keyspace::KeySpace,
|
||||||
|
pub sparse_keys: crate::keyspace::SparseKeySpace,
|
||||||
pub at_lsn: Lsn,
|
pub at_lsn: Lsn,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +34,8 @@ impl serde::Serialize for Partitioning {
|
|||||||
let mut map = serializer.serialize_map(Some(2))?;
|
let mut map = serializer.serialize_map(Some(2))?;
|
||||||
map.serialize_key("keys")?;
|
map.serialize_key("keys")?;
|
||||||
map.serialize_value(&KeySpace(&self.keys))?;
|
map.serialize_value(&KeySpace(&self.keys))?;
|
||||||
|
map.serialize_key("sparse_keys")?;
|
||||||
|
map.serialize_value(&KeySpace(&self.sparse_keys.0))?;
|
||||||
map.serialize_key("at_lsn")?;
|
map.serialize_key("at_lsn")?;
|
||||||
map.serialize_value(&WithDisplay(&self.at_lsn))?;
|
map.serialize_value(&WithDisplay(&self.at_lsn))?;
|
||||||
map.end()
|
map.end()
|
||||||
@@ -99,6 +103,7 @@ impl<'a> serde::Deserialize<'a> for Partitioning {
|
|||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct De {
|
struct De {
|
||||||
keys: KeySpace,
|
keys: KeySpace,
|
||||||
|
sparse_keys: KeySpace,
|
||||||
#[serde_as(as = "serde_with::DisplayFromStr")]
|
#[serde_as(as = "serde_with::DisplayFromStr")]
|
||||||
at_lsn: Lsn,
|
at_lsn: Lsn,
|
||||||
}
|
}
|
||||||
@@ -107,6 +112,7 @@ impl<'a> serde::Deserialize<'a> for Partitioning {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
at_lsn: de.at_lsn,
|
at_lsn: de.at_lsn,
|
||||||
keys: de.keys.0,
|
keys: de.keys.0,
|
||||||
|
sparse_keys: SparseKeySpace(de.sparse_keys.0),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,6 +139,12 @@ mod tests {
|
|||||||
"030000000000000000000000000000000003"
|
"030000000000000000000000000000000003"
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
"sparse_keys": [
|
||||||
|
[
|
||||||
|
"620000000000000000000000000000000000",
|
||||||
|
"620000000000000000000000000000000003"
|
||||||
|
]
|
||||||
|
],
|
||||||
"at_lsn": "0/2240160"
|
"at_lsn": "0/2240160"
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user