хранилище переехало с PocketBase на SQLite со своим каталогом файлов
- база своя: два пула, захват одним UPDATE ... RETURNING, шаги схемы на goose под файловым замком, одна миграция начальной схемы вместо семи прежних - транспорт переписан на net/http: свои слои, свой ограничитель частоты, отдача файла с проверкой владельца; панель /_/ и пространство /api/ исчезли - по находкам ревью: журнал не пишет путь под корнем приложения, ключ бюджета читается справа налево, узнавание известного идёт читающим пулом
This commit is contained in:
@@ -64,10 +64,38 @@ type ServerConfig struct {
|
||||
ForceShutdownTimeout int `toml:"force_shutdown_timeout"`
|
||||
}
|
||||
|
||||
// StorageConfig — единственный каталог данных: под ним лежат и база, и файлы
|
||||
// записей. Двух путей, как было раньше, у хранилища не бывает.
|
||||
// StorageConfig — хранилище сервиса: каталог данных и числа его базы.
|
||||
//
|
||||
// Каталог единственный: под ним лежат и база, и файлы записей. Двух путей, как
|
||||
// было раньше, у хранилища не бывает.
|
||||
type StorageConfig struct {
|
||||
DataDir string `toml:"data_dir"`
|
||||
// BusyTimeoutMs — сколько ждать занятую базу, миллисекунды.
|
||||
//
|
||||
// Ключом, а не константой кода: крутят его при отказе «база занята» под
|
||||
// несколькими воркерами, и подбор ответа на такой отказ не должен требовать
|
||||
// пересборки образа.
|
||||
BusyTimeoutMs int `toml:"busy_timeout_ms"`
|
||||
// ReadConnections — сколько соединений держит читающий пул. Пишущее
|
||||
// соединение при этом всегда одно, и настройкой оно не делается: драйвер
|
||||
// пишет единственным соединением, и второе означало бы отказы по занятости.
|
||||
ReadConnections int `toml:"read_connections"`
|
||||
}
|
||||
|
||||
// Validate проверяет настройки хранилища. Ноль и отрицательное — опечатка, а не
|
||||
// режим: нулевое ожидание отдаёт «база занята» первому же воркеру, а нулевой пул
|
||||
// чтения означает пул без предела, то есть настройку, которой не управляют.
|
||||
func (c StorageConfig) Validate() error {
|
||||
if strings.TrimSpace(c.DataDir) == "" {
|
||||
return errors.New("storage: не заполнен ключ data_dir: сервису негде держать базу и файлы записей")
|
||||
}
|
||||
if c.BusyTimeoutMs <= 0 {
|
||||
return errors.New("storage: busy_timeout_ms задаётся положительным числом миллисекунд")
|
||||
}
|
||||
if c.ReadConnections <= 0 {
|
||||
return errors.New("storage: read_connections задаётся положительным числом соединений")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type YandexConfig struct {
|
||||
@@ -156,6 +184,13 @@ func defaultConfig() *Config {
|
||||
},
|
||||
Storage: StorageConfig{
|
||||
DataDir: "data",
|
||||
// Пять секунд ожидания и четыре читающих соединения: числа выведены
|
||||
// из числа воркеров по умолчанию, а не из замера. Смысл ожидания —
|
||||
// пережить чужую запись, а не чужую работу: пишет сервис короткими
|
||||
// операциями, и очередь из трёх воркеров укладывается в него с
|
||||
// запасом.
|
||||
BusyTimeoutMs: 5000,
|
||||
ReadConnections: 4,
|
||||
},
|
||||
Pipeline: PipelineConfig{
|
||||
Workers: 3,
|
||||
|
||||
@@ -216,3 +216,42 @@ func TestPipelineValidateSeparatesModeFromTypo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Настройки хранилища проверяются на старте, и каждая ветвь проверки закрывает
|
||||
// свою поломку. Ноль и отрицательное — опечатка, а не режим: нулевое ожидание
|
||||
// отдаёт «база занята» первому же воркеру, нулевой пул чтения означает пул без
|
||||
// предела, а пустой каталог данных оставляет сервис без места под базу и файлы.
|
||||
// Без проверки такая опечатка проявилась бы отказом под нагрузкой, а не на
|
||||
// подъёме.
|
||||
func TestStorageConfigValidate(t *testing.T) {
|
||||
valid := StorageConfig{DataDir: "data", BusyTimeoutMs: 5000, ReadConnections: 4}
|
||||
|
||||
if err := valid.Validate(); err != nil {
|
||||
t.Fatalf("заполненные настройки отвергнуты: %v", err)
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
config StorageConfig
|
||||
mention string
|
||||
}{
|
||||
{"каталог данных не заполнен", StorageConfig{DataDir: "", BusyTimeoutMs: 5000, ReadConnections: 4}, "data_dir"},
|
||||
{"каталог данных из одних пробелов", StorageConfig{DataDir: " ", BusyTimeoutMs: 5000, ReadConnections: 4}, "data_dir"},
|
||||
{"ожидание нулевое", StorageConfig{DataDir: "data", BusyTimeoutMs: 0, ReadConnections: 4}, "busy_timeout_ms"},
|
||||
{"ожидание отрицательное", StorageConfig{DataDir: "data", BusyTimeoutMs: -1, ReadConnections: 4}, "busy_timeout_ms"},
|
||||
{"пул чтения нулевой", StorageConfig{DataDir: "data", BusyTimeoutMs: 5000, ReadConnections: 0}, "read_connections"},
|
||||
{"пул чтения отрицательный", StorageConfig{DataDir: "data", BusyTimeoutMs: 5000, ReadConnections: -3}, "read_connections"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := tc.config.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("негодная настройка принята: поломка дошла бы до боя")
|
||||
}
|
||||
if !strings.Contains(err.Error(), tc.mention) {
|
||||
t.Fatalf("имя ключа %q не названо: %v", tc.mention, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user