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