mirror of
https://github.com/amadvance/snapraid.git
synced 2026-10-26 19:01:57 -04:00
The new content file format now includes metadata for previous versions of files. This allows the fix command to access historical file data from snapshots as needed, ensuring recovery even if the live filesystem has changed or the file was deleted during a sync.
374 lines
10 KiB
C
374 lines
10 KiB
C
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
// Copyright (C) 2025 Andrea Mazzoleni
|
|
|
|
#include "portable.h"
|
|
|
|
#include "thermal.h"
|
|
#include "state.h"
|
|
#include "io.h"
|
|
|
|
#include <math.h>
|
|
|
|
struct snapraid_thermal* thermal_alloc(uint64_t dev, const char* name)
|
|
{
|
|
struct snapraid_thermal* thermal = malloc_nofail(sizeof(struct snapraid_thermal));
|
|
|
|
thermal->device = dev;
|
|
thermal->latest_temperature = 0;
|
|
thermal->count = 0;
|
|
pathcpy(thermal->name, sizeof(thermal->name), name);
|
|
|
|
return thermal;
|
|
}
|
|
|
|
void thermal_free(struct snapraid_thermal* thermal)
|
|
{
|
|
free(thermal);
|
|
}
|
|
|
|
/*
|
|
* Fit exponential heating model to data using least squares
|
|
*/
|
|
struct snapraid_thermal_params fit_thermal_model(const struct snapraid_thermal_point* points, int n_points, double t_ambient)
|
|
{
|
|
struct snapraid_thermal_params model;
|
|
double t_steady_try;
|
|
double k_try;
|
|
|
|
memset(&model, 0, sizeof(model));
|
|
|
|
model.t_ambient = t_ambient;
|
|
|
|
/* at least four points to have a result */
|
|
if (n_points < 4)
|
|
return model;
|
|
|
|
double last_temp = points[n_points - 1].temperature;
|
|
|
|
/* iterative refinement to find best k_heat and t_steady */
|
|
double best_error = 1e10;
|
|
double best_k = 0;
|
|
double best_t_steady = 0;
|
|
|
|
/* grid search for parameters */
|
|
for (t_steady_try = last_temp + 2.0; t_steady_try <= last_temp + 25.0; t_steady_try += 0.5) {
|
|
for (k_try = 0.00001; k_try <= 0.001; k_try *= 1.2) {
|
|
double error = 0.0;
|
|
int i;
|
|
|
|
/* calculate error for this parameter set */
|
|
for (i = 0; i < n_points; i++) {
|
|
double t = points[i].time;
|
|
double t_predicted = t_steady_try - (t_steady_try - points[0].temperature) * exp(-k_try * t);
|
|
double diff = points[i].temperature - t_predicted;
|
|
error += diff * diff;
|
|
}
|
|
|
|
if (error < best_error) {
|
|
best_error = error;
|
|
best_k = k_try;
|
|
best_t_steady = t_steady_try;
|
|
}
|
|
}
|
|
}
|
|
|
|
model.k_heat = best_k;
|
|
model.t_steady = best_t_steady;
|
|
|
|
/* calculate quality metrics */
|
|
double sum_squared_residuals = 0.0;
|
|
double sum_total = 0.0;
|
|
double mean_temp = 0.0;
|
|
model.max_error = 0.0;
|
|
int i;
|
|
|
|
/* calculate mean temperature */
|
|
for (i = 0; i < n_points; i++)
|
|
mean_temp += points[i].temperature;
|
|
mean_temp /= n_points;
|
|
|
|
/* calculate R-squared and errors */
|
|
for (i = 0; i < n_points; i++) {
|
|
double t = points[i].time;
|
|
double t_predicted = model.t_steady - (model.t_steady - points[0].temperature) * exp(-model.k_heat * t);
|
|
double residual = points[i].temperature - t_predicted;
|
|
|
|
sum_squared_residuals += residual * residual;
|
|
sum_total += (points[i].temperature - mean_temp) * (points[i].temperature - mean_temp);
|
|
|
|
double abs_error = fabs(residual);
|
|
if (abs_error > model.max_error) {
|
|
model.max_error = abs_error;
|
|
}
|
|
}
|
|
|
|
model.rmse = sqrt(sum_squared_residuals / n_points);
|
|
model.r_squared = 1.0 - (sum_squared_residuals / sum_total);
|
|
|
|
return model;
|
|
}
|
|
|
|
void state_thermal(struct snapraid_state* state, time_t now)
|
|
{
|
|
tommy_node* i;
|
|
unsigned j;
|
|
tommy_list high;
|
|
tommy_list low;
|
|
int ret;
|
|
|
|
if (state->thermal_temperature_limit == 0)
|
|
return;
|
|
|
|
tommy_list_init(&high);
|
|
tommy_list_init(&low);
|
|
|
|
/* for all disks */
|
|
for (i = state->disklist; i != 0; i = i->next) {
|
|
struct snapraid_disk* disk = i->data;
|
|
devinfo_t* entry;
|
|
|
|
entry = calloc_nofail(1, sizeof(devinfo_t));
|
|
|
|
entry->device = disk->mount_device;
|
|
device_name_set(entry, disk->name, 0);
|
|
pathcpy(entry->mount, sizeof(entry->mount), disk->mount_point);
|
|
pathcpy(entry->smartctl, sizeof(entry->smartctl), disk->smartctl);
|
|
memcpy(entry->smartignore, disk->smartignore, sizeof(entry->smartignore));
|
|
|
|
tommy_list_insert_tail(&high, &entry->node, entry);
|
|
}
|
|
|
|
/* for all parities */
|
|
for (j = 0; j < state->level; ++j) {
|
|
devinfo_t* entry;
|
|
unsigned s;
|
|
|
|
for (s = 0; s < state->parity[j].split_mac; ++s) {
|
|
entry = calloc_nofail(1, sizeof(devinfo_t));
|
|
|
|
entry->device = state->parity[j].split_map[s].device;
|
|
device_name_set(entry, lev_config_name(j), s);
|
|
pathcpy(entry->mount, sizeof(entry->mount), state->parity[j].split_map[s].path);
|
|
pathcpy(entry->smartctl, sizeof(entry->smartctl), state->parity[j].smartctl);
|
|
memcpy(entry->smartignore, state->parity[j].smartignore, sizeof(entry->smartignore));
|
|
pathcut(entry->mount); /* remove the parity file */
|
|
|
|
tommy_list_insert_tail(&high, &entry->node, entry);
|
|
}
|
|
}
|
|
|
|
/* with a GUI always gives time reference */
|
|
if (state->opt.gui)
|
|
log_tag("unixtime:%" PRIi64 "\n", (int64_t)now);
|
|
|
|
if (state->opt.fake_device) {
|
|
ret = devtest(&high, &low, DEVICE_SMART);
|
|
} else {
|
|
ret = devquery(&high, &low, DEVICE_SMART);
|
|
}
|
|
|
|
/* on error, just disable thermal gathering */
|
|
if (ret != 0)
|
|
return;
|
|
|
|
/* if the list is empty, it's not supported in this platform */
|
|
if (tommy_list_empty(&low))
|
|
return;
|
|
|
|
/* report new attribute */
|
|
state_attr(state, &low);
|
|
|
|
/* if ambient temperature is not set, set it now with the lowest HD temperature */
|
|
if (state->thermal_ambient_temperature == 0) {
|
|
state->thermal_ambient_temperature = ambient_temperature();
|
|
|
|
for (i = tommy_list_head(&low); i != 0; i = i->next) {
|
|
devinfo_t* devinfo = i->data;
|
|
|
|
int temp = smart_temp(devinfo);
|
|
if (temp < 0)
|
|
continue;
|
|
|
|
log_tag("thermal:system:candidate:%d\n", temp);
|
|
|
|
if (state->thermal_ambient_temperature == 0 || state->thermal_ambient_temperature > temp)
|
|
state->thermal_ambient_temperature = temp;
|
|
}
|
|
|
|
log_tag("thermal:system:final:%d\n", state->thermal_ambient_temperature);
|
|
}
|
|
|
|
int highest_temperature = 0;
|
|
for (i = tommy_list_head(&low); i != 0; i = i->next) {
|
|
tommy_node* t;
|
|
struct snapraid_thermal* found;
|
|
devinfo_t* devinfo = i->data;
|
|
unsigned k;
|
|
|
|
int temperature = smart_temp(devinfo);
|
|
if (temperature < 0)
|
|
continue;
|
|
|
|
/* search of the entry */
|
|
found = 0;
|
|
for (t = tommy_list_head(&state->thermallist); t != 0; t = t->next) {
|
|
struct snapraid_thermal* thermal = t->data;
|
|
if (thermal->device == devinfo->device) {
|
|
found = thermal;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* if not found, create it */
|
|
if (found == 0) {
|
|
found = thermal_alloc(devinfo->device, devinfo->name);
|
|
tommy_list_insert_tail(&state->thermallist, &found->node, found);
|
|
}
|
|
|
|
found->latest_temperature = temperature;
|
|
|
|
if (highest_temperature < temperature)
|
|
highest_temperature = temperature;
|
|
|
|
log_tag("thermal:current:%s:%" PRIu64 ":%d\n", devinfo->name, devinfo->device, temperature);
|
|
|
|
if (state->thermal_stop_gathering)
|
|
continue;
|
|
|
|
if (found->count + 1 >= THERMAL_MAX) /* keep one extra space at the end */
|
|
continue;
|
|
|
|
/* only monotone increasing temperature */
|
|
if (found->count > 0 && found->data[found->count - 1].temperature >= temperature)
|
|
continue;
|
|
|
|
/* insert the new data point */
|
|
found->data[found->count].temperature = temperature;
|
|
found->data[found->count].time = now - state->thermal_first;
|
|
++found->count;
|
|
|
|
if (state->opt.fake_device) {
|
|
/* fill with fake data */
|
|
found->data[0].time = 0;
|
|
found->data[0].temperature = 27;
|
|
found->data[1].time = 100;
|
|
found->data[1].temperature = 28;
|
|
found->data[2].time = 300;
|
|
found->data[2].temperature = 29;
|
|
found->data[3].time = 700;
|
|
found->data[3].temperature = 30;
|
|
found->data[4].time = 1500;
|
|
found->data[4].temperature = 31;
|
|
found->count = 5;
|
|
}
|
|
|
|
/* log the new data */
|
|
log_tag("thermal:heat:%s:%" PRIu64 ":%u:", devinfo->name, devinfo->device, found->count);
|
|
for (k = 0; k < found->count; ++k)
|
|
log_tag("%s%d/%d", k > 0 ? "," : "", (int)found->data[k].temperature, (int)found->data[k].time);
|
|
log_tag("\n");
|
|
|
|
/* estimate parameters */
|
|
found->params = fit_thermal_model(found->data, found->count, state->thermal_ambient_temperature);
|
|
|
|
log_tag("thermal:params:%s:%" PRIu64 ":%g:%g:%g:%g:%g:%g\n", devinfo->name, devinfo->device,
|
|
found->params.k_heat, found->params.t_ambient, found->params.t_steady,
|
|
found->params.rmse, found->params.r_squared, found->params.max_error);
|
|
}
|
|
|
|
/* always update the highest temperature */
|
|
state->thermal_highest_temperature = highest_temperature;
|
|
|
|
log_tag("thermal:highest:%d\n", highest_temperature);
|
|
log_flush();
|
|
|
|
tommy_list_foreach(&high, free);
|
|
tommy_list_foreach(&low, free);
|
|
}
|
|
|
|
int state_thermal_alarm(struct snapraid_state* state)
|
|
{
|
|
/* if no limit, there is no thermal support */
|
|
if (state->thermal_temperature_limit == 0)
|
|
return 0;
|
|
|
|
if (state->thermal_highest_temperature <= state->thermal_temperature_limit)
|
|
return 0;
|
|
|
|
return 1;
|
|
}
|
|
|
|
void state_thermal_cooldown(struct snapraid_state* state)
|
|
{
|
|
int sleep_time = state->thermal_cooldown_time;
|
|
|
|
if (sleep_time == 0)
|
|
sleep_time = 5 * 60; /* default sleep time */
|
|
if (sleep_time < 3 * 60)
|
|
sleep_time = 3 * 60; /* minimum sleep time */
|
|
|
|
/* from now on, stop any further data gathering as the heating is interrupted */
|
|
state->thermal_stop_gathering = 1;
|
|
|
|
log_tag("thermal:spindown\n");
|
|
state_device(state, DEVICE_DOWN, 0);
|
|
|
|
msg_progress("Cooldown...\n");
|
|
|
|
log_tag("thermal:cooldown:%d\n", sleep_time);
|
|
printf("Waiting for %d minutes...\n", sleep_time / 60);
|
|
|
|
log_flush();
|
|
|
|
/* every 30 seconds spin down any disk that was spunup */
|
|
while (sleep_time > 0) {
|
|
state_device(state, DEVICE_DOWNIFUP, 0);
|
|
|
|
sleep(30);
|
|
sleep_time -= 30;
|
|
}
|
|
|
|
if (!global_interrupt) { /* don't wake-up if we are interrupting */
|
|
log_tag("thermal:spinup\n");
|
|
|
|
/* spinup */
|
|
state_device(state, DEVICE_UP, 0);
|
|
|
|
/* log new thermal info */
|
|
state_thermal(state, 0);
|
|
}
|
|
}
|
|
|
|
int state_thermal_begin(struct snapraid_state* state, time_t now)
|
|
{
|
|
if (state->thermal_temperature_limit == 0)
|
|
return 1;
|
|
|
|
/* initial thermal measure */
|
|
state->thermal_first = now;
|
|
state->thermal_latest = now;
|
|
state_thermal(state, now);
|
|
|
|
if (state->thermal_ambient_temperature != 0) {
|
|
printf("System temperature is %u degrees\n", state->thermal_ambient_temperature);
|
|
|
|
if (state->thermal_temperature_limit != 0 && state->thermal_temperature_limit <= state->thermal_ambient_temperature) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EENVIRONMENT, "DANGER! System temperature of %d degrees is higher than the temperature limit of %d degrees. Unable to proceed!\n", state->thermal_ambient_temperature, state->thermal_temperature_limit);
|
|
log_flush();
|
|
return 0;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
}
|
|
|
|
if (state_thermal_alarm(state)) {
|
|
/* LCOV_EXCL_START */
|
|
log_fatal(EENVIRONMENT, "DANGER! Hard disk temperature of %d degrees is already outside the operating range. Unable to proceed!\n", state->thermal_highest_temperature);
|
|
log_flush();
|
|
return 0;
|
|
/* LCOV_EXCL_STOP */
|
|
}
|
|
|
|
return 1;
|
|
}
|
|
|