1 // SPDX-License-Identifier: CDDL-1.0
2 /*
3 * This file and its contents are supplied under the terms of the
4 * Common Development and Distribution License ("CDDL"), version 1.0.
5 * You may only use this file in accordance with the terms of version
6 * 1.0 of the CDDL.
7 *
8 * A full copy of the text of the CDDL should have accompanied this
9 * source. A copy of the CDDL is also available via the Internet at
10 * https://opensource.org/license/CDDL-1.0.
11 */
12 /*
13 * Copyright (c) 2024 Klara Inc.
14 *
15 * This software was developed by
16 * Mariusz Zaborski <mariusz.zaborski@klarasystems.com>
17 * Fred Weigel <fred.weigel@klarasystems.com>
18 * under sponsorship from Wasabi Technology, Inc. and Klara Inc.
19 */
20 /*
21 * This file implements a round-robin database that stores timestamps and txg
22 * numbers. Due to limited space, we use a round-robin approach, where
23 * the oldest records are overwritten when there is no longer enough room.
24 * This is a best-effort mechanism, and the database should be treated as
25 * an approximation. Consider this before consuming it.
26 *
27 * The database is linear, meaning we assume each new entry is newer than the
28 * ones already stored. Because of this, if time is manipulated, the database
29 * will only accept records that are newer than the existing ones.
30 * (For example, jumping 10 years into the future and then back can lead to
31 * situation when for 10 years we wont write anything to database)
32 *
33 * All times stored in the database use UTC, which makes it easy to convert to
34 * and from local time.
35 *
36 * Each database holds 256 records (as defined in the `RRD_MAX_ENTRIES` macro).
37 * This limit comes from the maximum size of a ZAP object, where we store the
38 * binary blob.
39 *
40 * We've split the database into three smaller ones.
41 * The `minute database` provides high resolution (default: every 10 minutes),
42 * but only covers approximately 1.5 days. This gives a detailed view of recent
43 * activity, useful, for example, when performing a scrub of the last hour.
44 * The `daily database` records one txg per day. With 256 entries, it retains
45 * roughly 8 months of data. This allows users to scrub or analyze txgs across
46 * a range of days.
47 * The `monthly database` stores one record per month, giving approximately
48 * 21 years of history.
49 * All these calculations assume the worst-case scenario: the pool is always
50 * online and actively written to.
51 *
52 * A potential source of confusion is that the database does not store data
53 * while the pool is offline, leading to potential gaps in timeline. Also,
54 * the database contains no records from before this feature was enabled.
55 * Both, upon reflection, are expected.
56 */
57 #include <sys/zfs_context.h>
58
59 #include "zfs_crrd.h"
60
61 rrd_data_t *
rrd_tail_entry(rrd_t * rrd)62 rrd_tail_entry(rrd_t *rrd)
63 {
64 size_t n;
65
66 if (rrd_len(rrd) == 0)
67 return (NULL);
68
69 if (rrd->rrd_tail == 0)
70 n = RRD_MAX_ENTRIES - 1;
71 else
72 n = rrd->rrd_tail - 1;
73
74 return (&rrd->rrd_entries[n]);
75 }
76
77 uint64_t
rrd_tail(rrd_t * rrd)78 rrd_tail(rrd_t *rrd)
79 {
80 const rrd_data_t *tail;
81
82 tail = rrd_tail_entry(rrd);
83
84 return (tail == NULL ? 0 : tail->rrdd_time);
85 }
86
87 /*
88 * Return length of data in the rrd.
89 * rrd_get works from 0..rrd_len()-1.
90 */
91 size_t
rrd_len(const rrd_t * rrd)92 rrd_len(const rrd_t *rrd)
93 {
94
95 return (rrd->rrd_length);
96 }
97
98 const rrd_data_t *
rrd_entry(const rrd_t * rrd,size_t i)99 rrd_entry(const rrd_t *rrd, size_t i)
100 {
101 size_t n;
102
103 if (i >= rrd_len(rrd)) {
104 return (0);
105 }
106
107 n = (rrd->rrd_head + i) % RRD_MAX_ENTRIES;
108 return (&rrd->rrd_entries[n]);
109 }
110
111 uint64_t
rrd_get(const rrd_t * rrd,size_t i)112 rrd_get(const rrd_t *rrd, size_t i)
113 {
114 const rrd_data_t *data = rrd_entry(rrd, i);
115
116 return (data == NULL ? 0 : data->rrdd_txg);
117 }
118
119 /* Add value to database. */
120 void
rrd_add(rrd_t * rrd,hrtime_t time,uint64_t txg)121 rrd_add(rrd_t *rrd, hrtime_t time, uint64_t txg)
122 {
123 rrd_data_t *tail;
124
125 tail = rrd_tail_entry(rrd);
126 if (tail != NULL && tail->rrdd_time == time) {
127 if (tail->rrdd_txg < txg) {
128 tail->rrdd_txg = txg;
129 } else {
130 return;
131 }
132 }
133
134 rrd->rrd_entries[rrd->rrd_tail].rrdd_time = time;
135 rrd->rrd_entries[rrd->rrd_tail].rrdd_txg = txg;
136
137 rrd->rrd_tail = (rrd->rrd_tail + 1) % RRD_MAX_ENTRIES;
138
139 if (rrd->rrd_length < RRD_MAX_ENTRIES) {
140 rrd->rrd_length++;
141 } else {
142 rrd->rrd_head = (rrd->rrd_head + 1) % RRD_MAX_ENTRIES;
143 }
144 }
145
146 void
dbrrd_add(dbrrd_t * db,hrtime_t time,uint64_t txg)147 dbrrd_add(dbrrd_t *db, hrtime_t time, uint64_t txg)
148 {
149 hrtime_t daydiff, monthdiff, minutedif;
150
151 minutedif = time - rrd_tail(&db->dbr_minutes);
152 daydiff = time - rrd_tail(&db->dbr_days);
153 monthdiff = time - rrd_tail(&db->dbr_months);
154
155 if (monthdiff >= 0 && monthdiff >= 30 * 24 * 60 * 60)
156 rrd_add(&db->dbr_months, time, txg);
157 else if (daydiff >= 0 && daydiff >= 24 * 60 * 60)
158 rrd_add(&db->dbr_days, time, txg);
159 else if (minutedif >= 0)
160 rrd_add(&db->dbr_minutes, time, txg);
161 }
162
163 /*
164 * We could do a binary search here, but the routine isn't frequently
165 * called and the data is small so we stick to a simple loop.
166 */
167 static const rrd_data_t *
rrd_query(rrd_t * rrd,hrtime_t tv,dbrrd_rounding_t rounding)168 rrd_query(rrd_t *rrd, hrtime_t tv, dbrrd_rounding_t rounding)
169 {
170 const rrd_data_t *data = NULL;
171
172 for (size_t i = 0; i < rrd_len(rrd); i++) {
173 const rrd_data_t *cur = rrd_entry(rrd, i);
174
175 if (rounding == DBRRD_FLOOR) {
176 if (tv < cur->rrdd_time) {
177 break;
178 }
179 data = cur;
180 } else {
181 /* DBRRD_CEILING */
182 if (tv <= cur->rrdd_time) {
183 data = cur;
184 break;
185 }
186 }
187 }
188
189 return (data);
190 }
191
192 static const rrd_data_t *
dbrrd_closest(hrtime_t tv,const rrd_data_t * r1,const rrd_data_t * r2)193 dbrrd_closest(hrtime_t tv, const rrd_data_t *r1, const rrd_data_t *r2)
194 {
195
196 if (r1 == NULL)
197 return (r2);
198 if (r2 == NULL)
199 return (r1);
200
201 return (ABS(tv - (hrtime_t)r1->rrdd_time) <
202 ABS(tv - (hrtime_t)r2->rrdd_time) ? r1 : r2);
203 }
204
205 uint64_t
dbrrd_query(dbrrd_t * r,hrtime_t tv,dbrrd_rounding_t rounding)206 dbrrd_query(dbrrd_t *r, hrtime_t tv, dbrrd_rounding_t rounding)
207 {
208 const rrd_data_t *data, *dm, *dd, *dy;
209
210 data = NULL;
211 dm = rrd_query(&r->dbr_minutes, tv, rounding);
212 dd = rrd_query(&r->dbr_days, tv, rounding);
213 dy = rrd_query(&r->dbr_months, tv, rounding);
214
215 data = dbrrd_closest(tv, dbrrd_closest(tv, dd, dm), dy);
216
217 return (data == NULL ? 0 : data->rrdd_txg);
218 }
219
220 hrtime_t
dbrrd_latest_time(dbrrd_t * r)221 dbrrd_latest_time(dbrrd_t *r)
222 {
223 const rrd_data_t *head;
224 const rrd_t *curdb;
225 size_t dblen;
226
227 curdb = &r->dbr_minutes;
228 dblen = rrd_len(curdb);
229 if (dblen == 0)
230 return (0);
231
232 head = rrd_entry(curdb, dblen - 1);
233 return (head->rrdd_time);
234 }
235