Line data Source code
1 : //! Functions for handling per-tenant configuration options
2 : //!
3 : //! If tenant is created with --config option,
4 : //! the tenant-specific config will be stored in tenant's directory.
5 : //! Otherwise, global pageserver's config is used.
6 : //!
7 : //! If the tenant config file is corrupted, the tenant will be disabled.
8 : //! We cannot use global or default config instead, because wrong settings
9 : //! may lead to a data loss.
10 : //!
11 :
12 : use pageserver_api::models;
13 : use pageserver_api::shard::{ShardCount, ShardIdentity, ShardNumber, ShardStripeSize};
14 : use serde::{Deserialize, Serialize};
15 : use utils::critical;
16 : use utils::generation::Generation;
17 :
18 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
19 : pub(crate) enum AttachmentMode {
20 : /// Our generation is current as far as we know, and as far as we know we are the only attached
21 : /// pageserver. This is the "normal" attachment mode.
22 : Single,
23 : /// Our generation number is current as far as we know, but we are advised that another
24 : /// pageserver is still attached, and therefore to avoid executing deletions. This is
25 : /// the attachment mode of a pagesever that is the destination of a migration.
26 : Multi,
27 : /// Our generation number is superseded, or about to be superseded. We are advised
28 : /// to avoid remote storage writes if possible, and to avoid sending billing data. This
29 : /// is the attachment mode of a pageserver that is the origin of a migration.
30 : Stale,
31 : }
32 :
33 0 : #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
34 : pub(crate) struct AttachedLocationConfig {
35 : pub(crate) generation: Generation,
36 : pub(crate) attach_mode: AttachmentMode,
37 : // TODO: add a flag to override AttachmentMode's policies under
38 : // disk pressure (i.e. unblock uploads under disk pressure in Stale
39 : // state, unblock deletions after timeout in Multi state)
40 : }
41 :
42 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43 : pub(crate) struct SecondaryLocationConfig {
44 : /// If true, keep the local cache warm by polling remote storage
45 : pub(crate) warm: bool,
46 : }
47 :
48 0 : #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
49 : pub(crate) enum LocationMode {
50 : Attached(AttachedLocationConfig),
51 : Secondary(SecondaryLocationConfig),
52 : }
53 :
54 : /// Per-tenant, per-pageserver configuration. All pageservers use the same TenantConf,
55 : /// but have distinct LocationConf.
56 0 : #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
57 : pub(crate) struct LocationConf {
58 : /// The location-specific part of the configuration, describes the operating
59 : /// mode of this pageserver for this tenant.
60 : pub(crate) mode: LocationMode,
61 :
62 : /// The detailed shard identity. This structure is already scoped within
63 : /// a TenantShardId, but we need the full ShardIdentity to enable calculating
64 : /// key->shard mappings.
65 : ///
66 : /// NB: we store this even for unsharded tenants, so that we agree with storcon on the intended
67 : /// stripe size. Otherwise, a split request that does not specify a stripe size may use a
68 : /// different default than storcon, which can lead to incorrect stripe sizes and corruption.
69 : pub(crate) shard: ShardIdentity,
70 :
71 : /// The pan-cluster tenant configuration, the same on all locations
72 : pub(crate) tenant_conf: pageserver_api::models::TenantConfig,
73 : }
74 :
75 : impl std::fmt::Debug for LocationConf {
76 0 : fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 0 : match &self.mode {
78 0 : LocationMode::Attached(conf) => {
79 0 : write!(
80 0 : f,
81 0 : "Attached {:?}, gen={:?}",
82 : conf.attach_mode, conf.generation
83 : )
84 : }
85 0 : LocationMode::Secondary(conf) => {
86 0 : write!(f, "Secondary, warm={}", conf.warm)
87 : }
88 : }
89 0 : }
90 : }
91 :
92 : impl AttachedLocationConfig {
93 : /// Consult attachment mode to determine whether we are currently permitted
94 : /// to delete layers. This is only advisory, not required for data safety.
95 : /// See [`AttachmentMode`] for more context.
96 617 : pub(crate) fn may_delete_layers_hint(&self) -> bool {
97 : // TODO: add an override for disk pressure in AttachedLocationConfig,
98 : // and respect it here.
99 617 : match &self.attach_mode {
100 617 : AttachmentMode::Single => true,
101 : AttachmentMode::Multi | AttachmentMode::Stale => {
102 : // In Multi mode we avoid doing deletions because some other
103 : // attached pageserver might get 404 while trying to read
104 : // a layer we delete which is still referenced in their metadata.
105 : //
106 : // In Stale mode, we avoid doing deletions because we expect
107 : // that they would ultimately fail validation in the deletion
108 : // queue due to our stale generation.
109 0 : false
110 : }
111 : }
112 617 : }
113 :
114 : /// Whether we are currently hinted that it is worthwhile to upload layers.
115 : /// This is only advisory, not required for data safety.
116 : /// See [`AttachmentMode`] for more context.
117 240 : pub(crate) fn may_upload_layers_hint(&self) -> bool {
118 : // TODO: add an override for disk pressure in AttachedLocationConfig,
119 : // and respect it here.
120 240 : match &self.attach_mode {
121 240 : AttachmentMode::Single | AttachmentMode::Multi => true,
122 : AttachmentMode::Stale => {
123 : // In Stale mode, we avoid doing uploads because we expect that
124 : // our replacement pageserver will already have started its own
125 : // IndexPart that will never reference layers we upload: it is
126 : // wasteful.
127 0 : false
128 : }
129 : }
130 240 : }
131 : }
132 :
133 : impl LocationConf {
134 : /// For use when loading from a legacy configuration: presence of a tenant
135 : /// implies it is in AttachmentMode::Single, which used to be the only
136 : /// possible state. This function should eventually be removed.
137 119 : pub(crate) fn attached_single(
138 119 : tenant_conf: pageserver_api::models::TenantConfig,
139 119 : generation: Generation,
140 119 : shard_params: models::ShardParameters,
141 119 : ) -> Self {
142 119 : Self {
143 119 : mode: LocationMode::Attached(AttachedLocationConfig {
144 119 : generation,
145 119 : attach_mode: AttachmentMode::Single,
146 119 : }),
147 119 : shard: ShardIdentity::from_params(ShardNumber(0), shard_params),
148 119 : tenant_conf,
149 119 : }
150 119 : }
151 :
152 : /// For use when attaching/re-attaching: update the generation stored in this
153 : /// structure. If we were in a secondary state, promote to attached (posession
154 : /// of a fresh generation implies this).
155 0 : pub(crate) fn attach_in_generation(
156 0 : &mut self,
157 0 : mode: AttachmentMode,
158 0 : generation: Generation,
159 0 : stripe_size: ShardStripeSize,
160 0 : ) {
161 0 : match &mut self.mode {
162 0 : LocationMode::Attached(attach_conf) => {
163 0 : attach_conf.generation = generation;
164 0 : attach_conf.attach_mode = mode;
165 0 : }
166 : LocationMode::Secondary(_) => {
167 : // We are promoted to attached by the control plane's re-attach response
168 0 : self.mode = LocationMode::Attached(AttachedLocationConfig {
169 0 : generation,
170 0 : attach_mode: mode,
171 0 : })
172 : }
173 : }
174 :
175 : // This should never happen.
176 : // TODO: turn this into a proper assertion.
177 0 : if stripe_size != self.shard.stripe_size {
178 0 : critical!(
179 0 : "stripe size mismatch: {} != {}",
180 : self.shard.stripe_size,
181 : stripe_size,
182 : );
183 0 : }
184 :
185 0 : self.shard.stripe_size = stripe_size;
186 0 : }
187 :
188 0 : pub(crate) fn try_from(conf: &'_ models::LocationConfig) -> anyhow::Result<Self> {
189 0 : let tenant_conf = conf.tenant_conf.clone();
190 :
191 0 : fn get_generation(conf: &'_ models::LocationConfig) -> Result<Generation, anyhow::Error> {
192 0 : conf.generation
193 0 : .map(Generation::new)
194 0 : .ok_or_else(|| anyhow::anyhow!("Generation must be set when attaching"))
195 0 : }
196 :
197 0 : let mode = match &conf.mode {
198 : models::LocationConfigMode::AttachedMulti => {
199 : LocationMode::Attached(AttachedLocationConfig {
200 0 : generation: get_generation(conf)?,
201 0 : attach_mode: AttachmentMode::Multi,
202 : })
203 : }
204 : models::LocationConfigMode::AttachedSingle => {
205 : LocationMode::Attached(AttachedLocationConfig {
206 0 : generation: get_generation(conf)?,
207 0 : attach_mode: AttachmentMode::Single,
208 : })
209 : }
210 : models::LocationConfigMode::AttachedStale => {
211 : LocationMode::Attached(AttachedLocationConfig {
212 0 : generation: get_generation(conf)?,
213 0 : attach_mode: AttachmentMode::Stale,
214 : })
215 : }
216 : models::LocationConfigMode::Secondary => {
217 0 : anyhow::ensure!(conf.generation.is_none());
218 :
219 0 : let warm = conf
220 0 : .secondary_conf
221 0 : .as_ref()
222 0 : .map(|c| c.warm)
223 0 : .unwrap_or(false);
224 0 : LocationMode::Secondary(SecondaryLocationConfig { warm })
225 : }
226 : models::LocationConfigMode::Detached => {
227 : // Should not have been called: API code should translate this mode
228 : // into a detach rather than trying to decode it as a LocationConf
229 0 : return Err(anyhow::anyhow!("Cannot decode a Detached configuration"));
230 : }
231 : };
232 :
233 0 : let shard = if conf.shard_count == 0 {
234 : // NB: carry over the persisted stripe size instead of using the default. This doesn't
235 : // matter for most practical purposes, since unsharded tenants don't use the stripe
236 : // size, but can cause inconsistencies between storcon and Pageserver and cause manual
237 : // splits without `new_stripe_size` to use an unintended stripe size.
238 0 : ShardIdentity::unsharded_with_stripe_size(ShardStripeSize(conf.shard_stripe_size))
239 : } else {
240 0 : ShardIdentity::new(
241 0 : ShardNumber(conf.shard_number),
242 0 : ShardCount::new(conf.shard_count),
243 0 : ShardStripeSize(conf.shard_stripe_size),
244 0 : )?
245 : };
246 :
247 0 : Ok(Self {
248 0 : shard,
249 0 : mode,
250 0 : tenant_conf,
251 0 : })
252 0 : }
253 : }
254 :
255 : impl Default for LocationConf {
256 : // TODO: this should be removed once tenant loading can guarantee that we are never
257 : // loading from a directory without a configuration.
258 : // => tech debt since https://github.com/neondatabase/neon/issues/1555
259 0 : fn default() -> Self {
260 0 : Self {
261 0 : mode: LocationMode::Attached(AttachedLocationConfig {
262 0 : generation: Generation::none(),
263 0 : attach_mode: AttachmentMode::Single,
264 0 : }),
265 0 : tenant_conf: pageserver_api::models::TenantConfig::default(),
266 0 : shard: ShardIdentity::unsharded(),
267 0 : }
268 0 : }
269 : }
270 :
271 : #[cfg(test)]
272 : mod tests {
273 : #[test]
274 1 : fn serde_roundtrip_tenant_conf_opt() {
275 1 : let small_conf = pageserver_api::models::TenantConfig {
276 1 : gc_horizon: Some(42),
277 1 : ..Default::default()
278 1 : };
279 :
280 1 : let toml_form = toml_edit::ser::to_string(&small_conf).unwrap();
281 1 : assert_eq!(toml_form, "gc_horizon = 42\n");
282 1 : assert_eq!(small_conf, toml_edit::de::from_str(&toml_form).unwrap());
283 :
284 1 : let json_form = serde_json::to_string(&small_conf).unwrap();
285 1 : assert_eq!(json_form, "{\"gc_horizon\":42}");
286 1 : assert_eq!(small_conf, serde_json::from_str(&json_form).unwrap());
287 1 : }
288 : }
|