Line data Source code
1 : //! This module defines `RequestContext`, a structure that we use throughout
2 : //! the pageserver to propagate high-level context from places
3 : //! that _originate_ activity down to the shared code paths at the
4 : //! heart of the pageserver. It's inspired by Golang's `context.Context`.
5 : //!
6 : //! For example, in `Timeline::get(page_nr, lsn)` we need to answer the following questions:
7 : //! 1. What high-level activity ([`TaskKind`]) needs this page?
8 : //! We need that information as a categorical dimension for page access
9 : //! statistics, which we, in turn, need to guide layer eviction policy design.
10 : //! 2. How should we behave if, to produce the page image, we need to
11 : //! on-demand download a layer file ([`DownloadBehavior`]).
12 : //!
13 : //! [`RequestContext`] satisfies those needs.
14 : //! The current implementation is a small `struct` that is passed through
15 : //! the call chain by reference.
16 : //!
17 : //! ### Future Work
18 : //!
19 : //! However, we do not intend to stop here, since there are other needs that
20 : //! require carrying information from high to low levels of the app.
21 : //!
22 : //! Most importantly, **cancellation signaling** in response to
23 : //! 1. timeouts (page_service max response time) and
24 : //! 2. lifecycle requests (detach tenant, delete timeline).
25 : //!
26 : //! Related to that, there is sometimes a need to ensure that all tokio tasks spawned
27 : //! by the transitive callees of a request have finished. The keyword here
28 : //! is **Structured Concurrency**, and right now, we use `task_mgr` in most places,
29 : //! `TaskHandle` in some places, and careful code review around `FuturesUnordered`
30 : //! or `JoinSet` in other places.
31 : //!
32 : //! We do not yet have a systematic cancellation story in pageserver, and it is
33 : //! pretty clear that [`RequestContext`] will be responsible for that.
34 : //! So, the API already prepares for this role through the
35 : //! [`RequestContext::detached_child`] and [`RequestContext::attached_child`] methods.
36 : //! See their doc comments for details on how we will use them in the future.
37 : //!
38 : //! It is not clear whether or how we will enforce Structured Concurrency, and
39 : //! what role [`RequestContext`] will play there.
40 : //! So, the API doesn't prepare us for this topic.
41 : //!
42 : //! Other future uses of `RequestContext`:
43 : //! - Communicate compute & IO priorities (user-initiated request vs. background-loop)
44 : //! - Request IDs for distributed tracing
45 : //! - Request/Timeline/Tenant-scoped log levels
46 : //!
47 : //! RequestContext might look quite different once it supports those features.
48 : //! Likely, it will have a shape similar to Golang's `context.Context`.
49 : //!
50 : //! ### Why A Struct Instead Of Method Parameters
51 : //!
52 : //! What's typical about such information is that it needs to be passed down
53 : //! along the call chain from high level to low level, but few of the functions
54 : //! in the middle need to understand it.
55 : //! Further, it is to be expected that we will need to propagate more data
56 : //! in the future (see the earlier section on future work).
57 : //! Hence, for functions in the middle of the call chain, we have the following
58 : //! requirements:
59 : //! 1. It should be easy to forward the context to callees.
60 : //! 2. To propagate more data from high-level to low-level code, the functions in
61 : //! the middle should not need to be modified.
62 : //! The solution is to have a container structure ([`RequestContext`]) that
63 : //! carries the information. Functions that don't care about what's in it
64 : //! pass it along to callees.
65 : //!
66 : //! ### Why Not Task-Local Variables
67 : //!
68 : //! One could use task-local variables (the equivalent of thread-local variables)
69 : //! to address the immediate needs outlined above.
70 : //! However, we reject task-local variables because:
71 : //! 1. they are implicit, thereby making it harder to trace the data flow in code
72 : //! reviews and during debugging,
73 : //! 2. they can be mutable, which enables implicit return data flow,
74 : //! 3. they are restrictive in that code which fans out into multiple tasks,
75 : //! or even threads, needs to carefully propagate the state.
76 : //!
77 : //! In contrast, information flow with [`RequestContext`] is
78 : //! 1. always explicit,
79 : //! 2. strictly uni-directional because RequestContext is immutable,
80 : //! 3. tangible because a [`RequestContext`] is just a value.
81 : //! When creating child activities, regardless of whether it's a task,
82 : //! thread, or even an RPC to another service, the value can
83 : //! be used like any other argument.
84 : //!
85 : //! The solution is that all code paths are infected with precisely one
86 : //! [`RequestContext`] argument. Functions in the middle of the call chain
87 : //! only need to pass it on.
88 :
89 : use crate::task_mgr::TaskKind;
90 :
91 : pub(crate) mod optional_counter;
92 :
93 : // The main structure of this module, see module-level comment.
94 : #[derive(Debug)]
95 : pub struct RequestContext {
96 : task_kind: TaskKind,
97 : download_behavior: DownloadBehavior,
98 : access_stats_behavior: AccessStatsBehavior,
99 : page_content_kind: PageContentKind,
100 : pub micros_spent_throttled: optional_counter::MicroSecondsCounterU32,
101 : }
102 :
103 : /// The kind of access to the page cache.
104 12600 : #[derive(Clone, Copy, PartialEq, Eq, Debug, enum_map::Enum, strum_macros::IntoStaticStr)]
105 : pub enum PageContentKind {
106 : Unknown,
107 : DeltaLayerBtreeNode,
108 : DeltaLayerValue,
109 : ImageLayerBtreeNode,
110 : ImageLayerValue,
111 : InMemoryLayer,
112 : }
113 :
114 : /// Desired behavior if the operation requires an on-demand download
115 : /// to proceed.
116 : #[derive(Clone, Copy, PartialEq, Eq, Debug)]
117 : pub enum DownloadBehavior {
118 : /// Download the layer file. It can take a while.
119 : Download,
120 :
121 : /// Download the layer file, but print a warning to the log. This should be used
122 : /// in code where the layer file is expected to already exist locally.
123 : Warn,
124 :
125 : /// Return a PageReconstructError::NeedsDownload error
126 : Error,
127 : }
128 :
129 : /// Whether this request should update access times used in LRU eviction
130 : #[derive(Clone, Copy, PartialEq, Eq, Debug)]
131 : pub(crate) enum AccessStatsBehavior {
132 : /// Update access times: this request's access to data should be taken
133 : /// as a hint that the accessed layer is likely to be accessed again
134 : Update,
135 :
136 : /// Do not update access times: this request is accessing the layer
137 : /// but does not want to indicate that the layer should be retained in cache,
138 : /// perhaps because the requestor is a compaction routine that will soon cover
139 : /// this layer with another.
140 : Skip,
141 : }
142 :
143 : pub struct RequestContextBuilder {
144 : inner: RequestContext,
145 : }
146 :
147 : impl RequestContextBuilder {
148 : /// A new builder with default settings
149 2346 : pub fn new(task_kind: TaskKind) -> Self {
150 2346 : Self {
151 2346 : inner: RequestContext {
152 2346 : task_kind,
153 2346 : download_behavior: DownloadBehavior::Download,
154 2346 : access_stats_behavior: AccessStatsBehavior::Update,
155 2346 : page_content_kind: PageContentKind::Unknown,
156 2346 : micros_spent_throttled: Default::default(),
157 2346 : },
158 2346 : }
159 2346 : }
160 :
161 6122724 : pub fn extend(original: &RequestContext) -> Self {
162 6122724 : Self {
163 6122724 : // This is like a Copy, but avoid implementing Copy because ordinary users of
164 6122724 : // RequestContext should always move or ref it.
165 6122724 : inner: RequestContext {
166 6122724 : task_kind: original.task_kind,
167 6122724 : download_behavior: original.download_behavior,
168 6122724 : access_stats_behavior: original.access_stats_behavior,
169 6122724 : page_content_kind: original.page_content_kind,
170 6122724 : micros_spent_throttled: Default::default(),
171 6122724 : },
172 6122724 : }
173 6122724 : }
174 :
175 : /// Configure the DownloadBehavior of the context: whether to
176 : /// download missing layers, and/or warn on the download.
177 2346 : pub fn download_behavior(mut self, b: DownloadBehavior) -> Self {
178 2346 : self.inner.download_behavior = b;
179 2346 : self
180 2346 : }
181 :
182 : /// Configure the AccessStatsBehavior of the context: whether layer
183 : /// accesses should update the access time of the layer.
184 364 : pub(crate) fn access_stats_behavior(mut self, b: AccessStatsBehavior) -> Self {
185 364 : self.inner.access_stats_behavior = b;
186 364 : self
187 364 : }
188 :
189 6122360 : pub(crate) fn page_content_kind(mut self, k: PageContentKind) -> Self {
190 6122360 : self.inner.page_content_kind = k;
191 6122360 : self
192 6122360 : }
193 :
194 6125070 : pub fn build(self) -> RequestContext {
195 6125070 : self.inner
196 6125070 : }
197 : }
198 :
199 : impl RequestContext {
200 : /// Create a new RequestContext that has no parent.
201 : ///
202 : /// The function is called `new` because, once we add children
203 : /// to it using `detached_child` or `attached_child`, the context
204 : /// form a tree (not implemented yet since cancellation will be
205 : /// the first feature that requires a tree).
206 : ///
207 : /// # Future: Cancellation
208 : ///
209 : /// The only reason why a context like this one can be canceled is
210 : /// because someone explicitly canceled it.
211 : /// It has no parent, so it cannot inherit cancellation from there.
212 2346 : pub fn new(task_kind: TaskKind, download_behavior: DownloadBehavior) -> Self {
213 2346 : RequestContextBuilder::new(task_kind)
214 2346 : .download_behavior(download_behavior)
215 2346 : .build()
216 2346 : }
217 :
218 : /// Create a detached child context for a task that may outlive `self`.
219 : ///
220 : /// Use this when spawning new background activity that should complete
221 : /// even if the current request is canceled.
222 : ///
223 : /// # Future: Cancellation
224 : ///
225 : /// Cancellation of `self` will not propagate to the child context returned
226 : /// by this method.
227 : ///
228 : /// # Future: Structured Concurrency
229 : ///
230 : /// We could add the Future as a parameter to this function, spawn it as a task,
231 : /// and pass to the new task the child context as an argument.
232 : /// That would be an ergonomic improvement.
233 : ///
234 : /// We could make new calls to this function fail if `self` is already canceled.
235 208 : pub fn detached_child(&self, task_kind: TaskKind, download_behavior: DownloadBehavior) -> Self {
236 208 : self.child_impl(task_kind, download_behavior)
237 208 : }
238 :
239 : /// Create a child of context `self` for a task that shall not outlive `self`.
240 : ///
241 : /// Use this when fanning-out work to other async tasks.
242 : ///
243 : /// # Future: Cancellation
244 : ///
245 : /// Cancelling a context will propagate to its attached children.
246 : ///
247 : /// # Future: Structured Concurrency
248 : ///
249 : /// We could add the Future as a parameter to this function, spawn it as a task,
250 : /// and track its `JoinHandle` inside the `RequestContext`.
251 : ///
252 : /// We could then provide another method to allow waiting for all child tasks
253 : /// to finish.
254 : ///
255 : /// We could make new calls to this function fail if `self` is already canceled.
256 : /// Alternatively, we could allow the creation but not spawn the task.
257 : /// The method to wait for child tasks would return an error, indicating
258 : /// that the child task was not started because the context was canceled.
259 1472 : pub fn attached_child(&self) -> Self {
260 1472 : self.child_impl(self.task_kind(), self.download_behavior())
261 1472 : }
262 :
263 : /// Use this function when you should be creating a child context using
264 : /// [`attached_child`] or [`detached_child`], but your caller doesn't provide
265 : /// a context and you are unwilling to change all callers to provide one.
266 : ///
267 : /// Before we add cancellation, we should get rid of this method.
268 : ///
269 : /// [`attached_child`]: Self::attached_child
270 : /// [`detached_child`]: Self::detached_child
271 372 : pub fn todo_child(task_kind: TaskKind, download_behavior: DownloadBehavior) -> Self {
272 372 : Self::new(task_kind, download_behavior)
273 372 : }
274 :
275 1680 : fn child_impl(&self, task_kind: TaskKind, download_behavior: DownloadBehavior) -> Self {
276 1680 : Self::new(task_kind, download_behavior)
277 1680 : }
278 :
279 15684984 : pub fn task_kind(&self) -> TaskKind {
280 15684984 : self.task_kind
281 15684984 : }
282 :
283 1480 : pub fn download_behavior(&self) -> DownloadBehavior {
284 1480 : self.download_behavior
285 1480 : }
286 :
287 212589 : pub(crate) fn access_stats_behavior(&self) -> AccessStatsBehavior {
288 212589 : self.access_stats_behavior
289 212589 : }
290 :
291 14193796 : pub(crate) fn page_content_kind(&self) -> PageContentKind {
292 14193796 : self.page_content_kind
293 14193796 : }
294 : }
|