parent
0f50873f0f
commit
fbb30f31e8
@ -0,0 +1,132 @@ |
||||
1. Zip-архів із базою даних на 841 млн показань (791 МБ) можна скачати за посиланням https://drive.google.com/file/d/18oks6STkVpg4-TT2WyCIBBpCgRyF29L2 |
||||
|
||||
В архіві 5 файлів: |
||||
2.changes |
||||
2.snapshot |
||||
test.data |
||||
test.index |
||||
metrics.info |
||||
|
||||
Після розпаковки їх необхідно перемістити в директорію testdir. |
||||
|
||||
|
||||
2. Якщо є бажання експериментувати з пустою базою даних, краще створити нову. Наприклад, назвемо її "x": створюємо директорію xdir, створюємо файл x.ini із такими налаштуваннями: |
||||
tcpPort = 12345 |
||||
dir = xdir |
||||
redoDir = xdir |
||||
databaseName = x |
||||
|
||||
Запускаємо СУБД із термінала: |
||||
./database_linux -c x.ini |
||||
|
||||
Всі операції виконуємо в кореневій директорії проєкту. |
||||
|
||||
3. Файли *.ini: |
||||
database.ini - налаштування СУБД (database); |
||||
loadtest.ini - налаштування навантажувального тесту (loadtest); |
||||
requests.ini - налаштування прикладів запитів (requests). |
||||
|
||||
|
||||
4. Скомпільоване ПО для Linux (64-бітна архітектура): |
||||
database_linux - СУБД; |
||||
loadtest_linux - навантажувального тест; |
||||
requests_linux - приклади запитів. |
||||
|
||||
|
||||
5. Скомпільоване ПО для Windows (64-бітна архітектура): |
||||
database_windows - СУБД; |
||||
loadtest_windows - навантажувального тест; |
||||
requests_windows - приклади запитів. |
||||
|
||||
|
||||
6. Директорія examples має три вкладені директорії із вихідними кодами: |
||||
database - запуску СУБД; |
||||
loadtest - навантажувального тесту; |
||||
requests - різних типів запитів, що підтримуються СУБД. |
||||
|
||||
|
||||
7. Якщо на комп'ютері встановлено компілятор Go, можна скомпілювати згадані вище програми за допомогою bash-скриптів: |
||||
|
||||
./linux_build.sh |
||||
./windows_build.sh |
||||
|
||||
Скомпільовані версії програм опиняться в кореневій директорії проєкту. |
||||
|
||||
|
||||
8. Файли з даними розміщуються в директорії datadir. В базі даних вже записано 841 млн. показань. Сумарний розмір трохи більше 1.3 GB. |
||||
|
||||
|
||||
9. Налаштування навантажувального тесту можна змінити, відредагувавши файл loadtest.ini. |
||||
Опція connections - це кількість одночасно відкритих підключень до СУБД. |
||||
Опція requestsPerConn - це кількість запитів, які відправляє потік через одне відкрите підключення. |
||||
|
||||
Звіт виглядає наступним чином: |
||||
|
||||
TEST RESULTS: |
||||
Time: 2 seconds |
||||
Connections: 100 |
||||
Requests per conn: 500 |
||||
Total requests: 50000 |
||||
AVG request time: 3.121022ms |
||||
RPS: 26891 |
||||
|
||||
listCumulativeMeasures: 3099 (6.2%), AVG request time: 3.785916ms |
||||
listCumulativePeriods: 12055 (24.1%), AVG request time: 2.726391ms |
||||
listInstantMeasures: 1974 (3.9%), AVG request time: 6.726605ms |
||||
listInstantPeriods: 7710 (15.4%), AVG request time: 2.9808ms |
||||
listCurrentValues: 25162 (50.3%), AVG request time: 2.988301ms |
||||
|
||||
last day: 20382 (82.1%), AVG request time: 2.954718ms |
||||
last week: 2993 (12.1%), AVG request time: 4.050662ms |
||||
last month: 708 (2.9%), AVG request time: 8.248486ms |
||||
random time range: 755 (3.0%), AVG request time: 3.540239ms |
||||
|
||||
Навантажувальний тест відправляє випадкові запити до випадкових метрик. Оскільки на реальному проєкті запити та часові діапазони мають різну ймовірність - я реалізував це в навантажувальному тесті. |
||||
|
||||
Приклад 1: |
||||
listCurrentValues: 25162 (50.3%) , AVG request time: 2.988301ms |
||||
означає що відправлено 25162 запитів listCurrentValues, що склало 50.3% від загальної кількості запитів. Середній час виконання (Latency) запитів listCurrentValues склав 2.99 міллісекунди. |
||||
|
||||
Приклад 2: |
||||
last month: 708 (2.9%), AVG request time: 8.248486ms |
||||
означає що відправлено 708 запитів на отримання даних за останній місяць, що склало 2.9% від загальної кількості. Середній час виконання (Latency) таких запитів склав 8.25 міллісекунди. Часовий діапазон задається для всіх запитів окрім listCurrentValues. |
||||
|
||||
10. Запуск навантажувального тесту: |
||||
Виконуємо пункт 1. |
||||
|
||||
Запускаємо СУБД із термінала: |
||||
./database_linux |
||||
|
||||
Запускаємо тест із іншого термінала: |
||||
./loadtest_linux |
||||
|
||||
Чекаємо завершення. Звіт буде надруковано у терміналі після завершення тесту. |
||||
|
||||
Команди для Windows: |
||||
./database_windows |
||||
./loadtest_windows |
||||
|
||||
11. Запуск прикладів запитів: |
||||
Можна запускати на пустій базі даних (без скачування Zip-архіва із показаннями). |
||||
|
||||
Запускаємо СУБД із термінала: |
||||
./database_linux |
||||
|
||||
Запускаємо тест із іншого термінала: |
||||
./requests_linux |
||||
|
||||
Результат виконання запитів друкується у терміналі. |
||||
|
||||
Команди для Windows: |
||||
./database_windows |
||||
./requests_windows |
||||
|
||||
|
||||
12. Подивитись HELP: |
||||
./database_linux -h |
||||
./loadtest_linux -h |
||||
./requests_linux -h |
||||
|
||||
|
||||
13. Зупинити СУБД, перервати навантажувальний тест: |
||||
Ctrl + C |
@ -0,0 +1,325 @@ |
||||
package atree |
||||
|
||||
import ( |
||||
"fmt" |
||||
"time" |
||||
|
||||
"gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/timeutil" |
||||
) |
||||
|
||||
// AGGREGATE
|
||||
|
||||
type InstantAggregator struct { |
||||
firstHourOfDay int |
||||
lastDayOfMonth int |
||||
time2period func(uint32) uint32 |
||||
currentPeriod uint32 |
||||
since uint32 |
||||
until uint32 |
||||
min float64 |
||||
max float64 |
||||
total float64 |
||||
entries int |
||||
} |
||||
|
||||
type InstantAggregatorOptions struct { |
||||
GroupBy diploma.GroupBy |
||||
FirstHourOfDay int |
||||
LastDayOfMonth int |
||||
} |
||||
|
||||
func NewInstantAggregator(opt InstantAggregatorOptions) (*InstantAggregator, error) { |
||||
s := &InstantAggregator{ |
||||
firstHourOfDay: opt.FirstHourOfDay, |
||||
lastDayOfMonth: opt.LastDayOfMonth, |
||||
} |
||||
|
||||
switch opt.GroupBy { |
||||
case diploma.GroupByHour: |
||||
s.time2period = groupByHour |
||||
|
||||
case diploma.GroupByDay: |
||||
if s.firstHourOfDay > 0 { |
||||
s.time2period = s.groupByDayUsingFHD |
||||
} else { |
||||
s.time2period = groupByDay |
||||
} |
||||
|
||||
case diploma.GroupByMonth: |
||||
if s.firstHourOfDay > 0 { |
||||
if s.lastDayOfMonth > 0 { |
||||
s.time2period = s.groupByMonthUsingFHDAndLDM |
||||
} else { |
||||
s.time2period = s.groupByMonthUsingFHD |
||||
} |
||||
} else { |
||||
if s.lastDayOfMonth > 0 { |
||||
s.time2period = s.groupByMonthUsingLDM |
||||
} else { |
||||
s.time2period = groupByMonth |
||||
} |
||||
} |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown groupBy %d option", opt.GroupBy) |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
// Приходят данные от свежих к старым, тоесть сперва получаю Until.
|
||||
// return period complete flag
|
||||
func (s *InstantAggregator) Feed(timestamp uint32, value float64, p *InstantPeriod) bool { |
||||
period := s.time2period(timestamp) |
||||
//fmt.Printf("feed: %s %v, period: %s\n", time.Unix(int64(timestamp), 0), value, time.Unix(int64(period), 0))
|
||||
if s.entries == 0 { |
||||
s.currentPeriod = period |
||||
s.since = timestamp |
||||
s.until = timestamp |
||||
s.min = value |
||||
s.max = value |
||||
s.total = value |
||||
s.entries = 1 |
||||
return false |
||||
} |
||||
|
||||
if period != s.currentPeriod { |
||||
// готовый период
|
||||
s.FillPeriod(timestamp, p) |
||||
s.currentPeriod = period |
||||
s.since = timestamp |
||||
s.until = timestamp |
||||
s.min = value |
||||
s.max = value |
||||
s.total = value |
||||
s.entries = 1 |
||||
return true |
||||
} |
||||
|
||||
if value < s.min { |
||||
s.min = value |
||||
} else if value > s.max { |
||||
s.max = value |
||||
} |
||||
// для подсчета AVG
|
||||
s.total += value |
||||
s.entries++ |
||||
// начало периода
|
||||
s.since = timestamp |
||||
return false |
||||
} |
||||
|
||||
func (s *InstantAggregator) FillPeriod(prevTimestamp uint32, p *InstantPeriod) bool { |
||||
if s.entries == 0 { |
||||
return false |
||||
} |
||||
|
||||
//fmt.Printf("FillPeriod: %s, prevTimestamp: %s\n", time.Unix(int64(s.currentPeriod), 0), time.Unix(int64(prevTimestamp), 0))
|
||||
p.Period = s.currentPeriod |
||||
if prevTimestamp > 0 { |
||||
p.Since = prevTimestamp |
||||
} else { |
||||
p.Since = s.since |
||||
} |
||||
p.Until = s.until |
||||
p.Min = s.min |
||||
p.Max = s.max |
||||
p.Avg = s.total / float64(s.entries) |
||||
return true |
||||
} |
||||
|
||||
func (s *InstantAggregator) groupByDayUsingFHD(timestamp uint32) uint32 { |
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "d") |
||||
if tm.Hour() < s.firstHourOfDay { |
||||
tm = tm.AddDate(0, 0, -1) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
func (s *InstantAggregator) groupByMonthUsingFHD(timestamp uint32) uint32 { |
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m") |
||||
if tm.Hour() < s.firstHourOfDay { |
||||
tm = tm.AddDate(0, 0, -1) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
func (s *InstantAggregator) groupByMonthUsingLDM(timestamp uint32) uint32 { |
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m") |
||||
if tm.Day() > s.lastDayOfMonth { |
||||
tm = tm.AddDate(0, 1, 0) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
func (s *InstantAggregator) groupByMonthUsingFHDAndLDM(timestamp uint32) uint32 { |
||||
// ВАЖНО!
|
||||
// Сперва проверяю время.
|
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m") |
||||
if tm.Hour() < s.firstHourOfDay { |
||||
tm = tm.AddDate(0, 0, -1) |
||||
} |
||||
if tm.Day() > s.lastDayOfMonth { |
||||
tm = tm.AddDate(0, 1, 0) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
// CUMULATIVE
|
||||
|
||||
type CumulativeAggregator struct { |
||||
firstHourOfDay int |
||||
lastDayOfMonth int |
||||
time2period func(uint32) uint32 |
||||
currentPeriod uint32 |
||||
since uint32 |
||||
until uint32 |
||||
sinceValue float64 |
||||
untilValue float64 |
||||
entries int |
||||
} |
||||
|
||||
type CumulativeAggregatorOptions struct { |
||||
GroupBy diploma.GroupBy |
||||
FirstHourOfDay int |
||||
LastDayOfMonth int |
||||
} |
||||
|
||||
func NewCumulativeAggregator(opt CumulativeAggregatorOptions) (*CumulativeAggregator, error) { |
||||
s := &CumulativeAggregator{ |
||||
firstHourOfDay: opt.FirstHourOfDay, |
||||
lastDayOfMonth: opt.LastDayOfMonth, |
||||
} |
||||
|
||||
switch opt.GroupBy { |
||||
case diploma.GroupByHour: |
||||
s.time2period = groupByHour |
||||
|
||||
case diploma.GroupByDay: |
||||
if s.firstHourOfDay > 0 { |
||||
s.time2period = s.groupByDayUsingFHD |
||||
} else { |
||||
s.time2period = groupByDay |
||||
} |
||||
|
||||
case diploma.GroupByMonth: |
||||
if s.firstHourOfDay > 0 { |
||||
if s.lastDayOfMonth > 0 { |
||||
s.time2period = s.groupByMonthUsingFHDAndLDM |
||||
} else { |
||||
s.time2period = s.groupByMonthUsingFHD |
||||
} |
||||
} else { |
||||
if s.lastDayOfMonth > 0 { |
||||
s.time2period = s.groupByMonthUsingLDM |
||||
} else { |
||||
s.time2period = groupByMonth |
||||
} |
||||
} |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown groupBy %d option", opt.GroupBy) |
||||
} |
||||
|
||||
return s, nil |
||||
} |
||||
|
||||
// return period complete flag
|
||||
func (s *CumulativeAggregator) Feed(timestamp uint32, value float64, p *CumulativePeriod) bool { |
||||
period := s.time2period(timestamp) |
||||
if s.entries == 0 { |
||||
s.currentPeriod = period |
||||
s.since = timestamp |
||||
s.until = timestamp |
||||
s.sinceValue = value |
||||
s.untilValue = value |
||||
s.entries = 1 |
||||
return false |
||||
} |
||||
|
||||
if period != s.currentPeriod { |
||||
// готовый период
|
||||
s.FillPeriod(timestamp, value, p) |
||||
s.currentPeriod = period |
||||
s.since = timestamp |
||||
s.until = timestamp |
||||
s.sinceValue = value |
||||
s.untilValue = value |
||||
s.entries = 1 |
||||
return true |
||||
} |
||||
|
||||
// начало периода
|
||||
s.since = timestamp |
||||
s.sinceValue = value |
||||
s.entries++ |
||||
return false |
||||
} |
||||
|
||||
func (s *CumulativeAggregator) FillPeriod(prevTimestamp uint32, value float64, p *CumulativePeriod) bool { |
||||
if s.entries == 0 { |
||||
return false |
||||
} |
||||
p.Period = s.currentPeriod |
||||
if prevTimestamp > 0 { |
||||
p.Since = prevTimestamp |
||||
p.Total = s.untilValue - value |
||||
} else { |
||||
p.Since = s.since |
||||
p.Total = s.untilValue - s.sinceValue |
||||
} |
||||
p.Until = s.until |
||||
p.EndValue = s.untilValue |
||||
|
||||
return true |
||||
} |
||||
|
||||
func (s *CumulativeAggregator) groupByDayUsingFHD(timestamp uint32) uint32 { |
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "d") |
||||
if tm.Hour() < s.firstHourOfDay { |
||||
tm = tm.AddDate(0, 0, -1) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
func (s *CumulativeAggregator) groupByMonthUsingFHD(timestamp uint32) uint32 { |
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m") |
||||
if tm.Hour() < s.firstHourOfDay { |
||||
tm = tm.AddDate(0, 0, -1) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
func (s *CumulativeAggregator) groupByMonthUsingLDM(timestamp uint32) uint32 { |
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m") |
||||
if tm.Day() > s.lastDayOfMonth { |
||||
tm = tm.AddDate(0, 1, 0) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
func (s *CumulativeAggregator) groupByMonthUsingFHDAndLDM(timestamp uint32) uint32 { |
||||
// ВАЖНО!
|
||||
// Сперва проверяю время.
|
||||
tm := timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m") |
||||
if tm.Hour() < s.firstHourOfDay { |
||||
tm = tm.AddDate(0, 0, -1) |
||||
} |
||||
if tm.Day() > s.lastDayOfMonth { |
||||
tm = tm.AddDate(0, 1, 0) |
||||
} |
||||
return uint32(tm.Unix()) |
||||
} |
||||
|
||||
func groupByHour(timestamp uint32) uint32 { |
||||
return uint32(timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "h").Unix()) |
||||
} |
||||
|
||||
func groupByDay(timestamp uint32) uint32 { |
||||
return uint32(timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "d").Unix()) |
||||
} |
||||
|
||||
func groupByMonth(timestamp uint32) uint32 { |
||||
return uint32(timeutil.FirstSecondInPeriod(time.Unix(int64(timestamp), 0), "m").Unix()) |
||||
} |
@ -0,0 +1,497 @@ |
||||
package atree |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
"sync" |
||||
|
||||
"gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/atree/redo" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
const ( |
||||
filePerm = 0770 |
||||
|
||||
// index page
|
||||
indexRecordsQtyIdx = IndexPageSize - 7 |
||||
isDataPageNumbersIdx = IndexPageSize - 5 |
||||
indexCRC32Idx = IndexPageSize - 4 |
||||
|
||||
// data page
|
||||
timestampsSizeIdx = DataPageSize - 12 |
||||
valuesSizeIdx = DataPageSize - 10 |
||||
prevPageIdx = DataPageSize - 8 |
||||
dataCRC32Idx = DataPageSize - 4 |
||||
|
||||
timestampSize = 4 |
||||
pairSize = timestampSize + PageNoSize |
||||
indexFooterIdx = indexRecordsQtyIdx |
||||
dataFooterIdx = timestampsSizeIdx |
||||
|
||||
DataPageSize = 8192 |
||||
IndexPageSize = 1024 |
||||
PageNoSize = 4 |
||||
//
|
||||
DataPagePayloadSize int = dataFooterIdx |
||||
) |
||||
|
||||
type FreeList interface { |
||||
ReservePage() uint32 |
||||
} |
||||
|
||||
type _page struct { |
||||
PageNo uint32 |
||||
Buf []byte |
||||
ReferenceCount int |
||||
} |
||||
|
||||
type Atree struct { |
||||
redoDir string |
||||
indexFreelist FreeList |
||||
dataFreelist FreeList |
||||
dataFile *os.File |
||||
indexFile *os.File |
||||
mutex sync.Mutex |
||||
allocatedIndexPagesQty uint32 |
||||
allocatedDataPagesQty uint32 |
||||
indexPages map[uint32]*_page |
||||
dataPages map[uint32]*_page |
||||
indexWaits map[uint32][]chan readResult |
||||
dataWaits map[uint32][]chan readResult |
||||
indexPagesToRead []uint32 |
||||
dataPagesToRead []uint32 |
||||
readSignalCh chan struct{} |
||||
writeSignalCh chan struct{} |
||||
writeTasksQueue []WriteTask |
||||
} |
||||
|
||||
type Options struct { |
||||
Dir string |
||||
RedoDir string |
||||
DatabaseName string |
||||
DataFreeList FreeList |
||||
IndexFreeList FreeList |
||||
} |
||||
|
||||
func New(opt Options) (*Atree, error) { |
||||
if opt.Dir == "" { |
||||
return nil, errors.New("Dir option is required") |
||||
} |
||||
if opt.RedoDir == "" { |
||||
return nil, errors.New("RedoDir option is required") |
||||
} |
||||
if opt.DatabaseName == "" { |
||||
return nil, errors.New("DatabaseName option is required") |
||||
} |
||||
if opt.DataFreeList == nil { |
||||
return nil, errors.New("DataFreeList option is required") |
||||
} |
||||
if opt.IndexFreeList == nil { |
||||
return nil, errors.New("IndexFreeList option is required") |
||||
} |
||||
// открываю или создаю dbName.data и dbName.index файлы
|
||||
var ( |
||||
indexFileName = filepath.Join(opt.Dir, opt.DatabaseName+".index") |
||||
dataFileName = filepath.Join(opt.Dir, opt.DatabaseName+".data") |
||||
|
||||
indexFile *os.File |
||||
dataFile *os.File |
||||
allocatedIndexPagesQty uint32 |
||||
allocatedDataPagesQty uint32 |
||||
) |
||||
|
||||
// При создании data файла сразу создается индекс, поэтому корректное
|
||||
// состояние БД: либо оба файла есть, либо ни одного файла нет.
|
||||
isIndexExist, err := isFileExist(indexFileName) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("check index file is exist: %s", err) |
||||
} |
||||
|
||||
isDataExist, err := isFileExist(dataFileName) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("check data file is exist: %s", err) |
||||
} |
||||
|
||||
if isIndexExist { |
||||
if isDataExist { |
||||
// открываю оба файла
|
||||
indexFile, allocatedIndexPagesQty, err = openFile(indexFileName, IndexPageSize) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("open index file: %s", err) |
||||
} |
||||
|
||||
dataFile, allocatedDataPagesQty, err = openFile(dataFileName, DataPageSize) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("open data file: %s", err) |
||||
} |
||||
} else { |
||||
// нет data файла
|
||||
return nil, errors.New("not found data file") |
||||
} |
||||
} else { |
||||
if isDataExist { |
||||
// index файла нет
|
||||
return nil, errors.New("not found index file") |
||||
} else { |
||||
// нет обоих файлов
|
||||
indexFile, err = os.OpenFile(indexFileName, os.O_CREATE|os.O_RDWR, filePerm) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
dataFile, err = os.OpenFile(dataFileName, os.O_CREATE|os.O_RDWR, filePerm) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
} |
||||
|
||||
tree := &Atree{ |
||||
redoDir: opt.RedoDir, |
||||
indexFreelist: opt.IndexFreeList, |
||||
dataFreelist: opt.DataFreeList, |
||||
indexFile: indexFile, |
||||
dataFile: dataFile, |
||||
allocatedIndexPagesQty: allocatedIndexPagesQty, |
||||
allocatedDataPagesQty: allocatedDataPagesQty, |
||||
indexPages: make(map[uint32]*_page), |
||||
dataPages: make(map[uint32]*_page), |
||||
indexWaits: make(map[uint32][]chan readResult), |
||||
dataWaits: make(map[uint32][]chan readResult), |
||||
readSignalCh: make(chan struct{}, 1), |
||||
writeSignalCh: make(chan struct{}, 1), |
||||
} |
||||
|
||||
return tree, nil |
||||
} |
||||
|
||||
func (s *Atree) Run() { |
||||
go s.pageWriter() |
||||
go s.pageReader() |
||||
} |
||||
|
||||
// FIND
|
||||
|
||||
func (s *Atree) findDataPage(rootPageNo uint32, timestamp uint32) (uint32, []byte, error) { |
||||
indexPageNo := rootPageNo |
||||
for { |
||||
buf, err := s.fetchIndexPage(indexPageNo) |
||||
if err != nil { |
||||
return 0, nil, fmt.Errorf("fetchIndexPage(%d): %s", indexPageNo, err) |
||||
} |
||||
|
||||
foundPageNo := findPageNo(buf, timestamp) |
||||
s.releaseIndexPage(indexPageNo) |
||||
|
||||
if buf[isDataPageNumbersIdx] == 1 { |
||||
buf, err := s.fetchDataPage(foundPageNo) |
||||
if err != nil { |
||||
return 0, nil, fmt.Errorf("fetchDataPage(%d): %s", foundPageNo, err) |
||||
} |
||||
return foundPageNo, buf, nil |
||||
} |
||||
// вглубь
|
||||
indexPageNo = foundPageNo |
||||
} |
||||
} |
||||
|
||||
type pathLeg struct { |
||||
PageNo uint32 |
||||
Data []byte |
||||
} |
||||
|
||||
type pathToDataPage struct { |
||||
Legs []pathLeg |
||||
LastPageNo uint32 |
||||
} |
||||
|
||||
func (s *Atree) findPathToLastPage(rootPageNo uint32) (_ pathToDataPage, err error) { |
||||
var ( |
||||
pageNo = rootPageNo |
||||
legs []pathLeg |
||||
) |
||||
|
||||
for { |
||||
var buf []byte |
||||
buf, err = s.fetchIndexPage(pageNo) |
||||
if err != nil { |
||||
err = fmt.Errorf("FetchIndexPage(%d): %s", pageNo, err) |
||||
return |
||||
} |
||||
|
||||
legs = append(legs, pathLeg{ |
||||
PageNo: pageNo, |
||||
Data: buf, |
||||
// childIdx не нужен
|
||||
}) |
||||
|
||||
foundPageNo := getLastPageNo(buf) |
||||
|
||||
if buf[isDataPageNumbersIdx] == 1 { |
||||
return pathToDataPage{ |
||||
Legs: legs, |
||||
LastPageNo: foundPageNo, |
||||
}, nil |
||||
} |
||||
// вглубь
|
||||
pageNo = foundPageNo |
||||
} |
||||
} |
||||
|
||||
// APPEND DATA PAGE
|
||||
|
||||
type AppendDataPageReq struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
Since uint32 |
||||
RootPageNo uint32 |
||||
PrevPageNo uint32 |
||||
TimestampsChunks [][]byte |
||||
TimestampsSize uint16 |
||||
ValuesChunks [][]byte |
||||
ValuesSize uint16 |
||||
} |
||||
|
||||
func (s *Atree) AppendDataPage(req AppendDataPageReq) (_ redo.Report, err error) { |
||||
var ( |
||||
flags byte |
||||
dataPagesToRelease []uint32 |
||||
indexPagesToRelease []uint32 |
||||
) |
||||
|
||||
newDataPage := s.allocDataPage() |
||||
dataPagesToRelease = append(dataPagesToRelease, newDataPage.PageNo) |
||||
|
||||
chunksToDataPage(newDataPage.Data, chunksToDataPageReq{ |
||||
PrevPageNo: req.PrevPageNo, |
||||
TimestampsChunks: req.TimestampsChunks, |
||||
TimestampsSize: req.TimestampsSize, |
||||
ValuesChunks: req.ValuesChunks, |
||||
ValuesSize: req.ValuesSize, |
||||
}) |
||||
|
||||
redoWriter, err := redo.NewWriter(redo.WriterOptions{ |
||||
Dir: s.redoDir, |
||||
MetricID: req.MetricID, |
||||
Timestamp: req.Timestamp, |
||||
Value: req.Value, |
||||
IsDataPageReused: newDataPage.IsReused, |
||||
DataPageNo: newDataPage.PageNo, |
||||
Page: newDataPage.Data, |
||||
}) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
if req.RootPageNo > 0 { |
||||
var path pathToDataPage |
||||
path, err = s.findPathToLastPage(req.RootPageNo) |
||||
if err != nil { |
||||
return |
||||
} |
||||
for _, leg := range path.Legs { |
||||
indexPagesToRelease = append(indexPagesToRelease, leg.PageNo) |
||||
} |
||||
|
||||
if path.LastPageNo != req.PrevPageNo { |
||||
diploma.Abort( |
||||
diploma.WrongPrevPageNo, |
||||
fmt.Errorf("bug: last pageNo %d in tree != prev pageNo %d in _metric", |
||||
path.LastPageNo, req.PrevPageNo), |
||||
) |
||||
} |
||||
|
||||
newPageNo := newDataPage.PageNo |
||||
lastIdx := len(path.Legs) - 1 |
||||
|
||||
for legIdx := lastIdx; legIdx >= 0; legIdx-- { |
||||
leg := path.Legs[legIdx] |
||||
|
||||
ok := appendPair(leg.Data, req.Since, newPageNo) |
||||
if ok { |
||||
err = redoWriter.AppendIndexPage(leg.PageNo, leg.Data, 0) |
||||
if err != nil { |
||||
return |
||||
} |
||||
break |
||||
} |
||||
|
||||
newIndexPage := s.allocIndexPage() |
||||
indexPagesToRelease = append(indexPagesToRelease, newIndexPage.PageNo) |
||||
appendPair(newIndexPage.Data, req.Since, newPageNo) |
||||
// ставлю мітку що всі pageNo на сторінці - це data pageNo
|
||||
if legIdx == lastIdx { |
||||
newIndexPage.Data[isDataPageNumbersIdx] = 1 |
||||
} |
||||
|
||||
flags = 0 |
||||
if newIndexPage.IsReused { |
||||
flags |= redo.FlagReused |
||||
} |
||||
err = redoWriter.AppendIndexPage(newIndexPage.PageNo, newIndexPage.Data, flags) |
||||
if err != nil { |
||||
return |
||||
} |
||||
//
|
||||
newPageNo = newIndexPage.PageNo |
||||
|
||||
if legIdx == 0 { |
||||
newRoot := s.allocIndexPage() |
||||
indexPagesToRelease = append(indexPagesToRelease, newRoot.PageNo) |
||||
appendPair(newRoot.Data, getSince(leg.Data), leg.PageNo) // old rootPageNo
|
||||
appendPair(newRoot.Data, req.Since, newIndexPage.PageNo) |
||||
|
||||
// Фиксирую новый root в REDO логе
|
||||
flags = redo.FlagNewRoot |
||||
if newRoot.IsReused { |
||||
flags |= redo.FlagReused |
||||
} |
||||
err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags) |
||||
if err != nil { |
||||
return |
||||
} |
||||
break |
||||
} |
||||
} |
||||
} else { |
||||
newRoot := s.allocIndexPage() |
||||
indexPagesToRelease = append(indexPagesToRelease, newRoot.PageNo) |
||||
newRoot.Data[isDataPageNumbersIdx] = 1 |
||||
appendPair(newRoot.Data, req.Since, newDataPage.PageNo) |
||||
|
||||
flags = redo.FlagNewRoot |
||||
if newRoot.IsReused { |
||||
flags |= redo.FlagReused |
||||
} |
||||
err = redoWriter.AppendIndexPage(newRoot.PageNo, newRoot.Data, flags) |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
|
||||
err = redoWriter.Close() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
// На данний момен схема - наступна. Всі сторінки - data та index - зафіксовані в кеші.
|
||||
// Отже запис на диск пройде максимально швидко. Після цього ReferenceCount кожної
|
||||
// сторінки зменшиться на 1. Оскільки на метрику утримується XLock, сторінки мають
|
||||
// ReferenceCount = 1 (немає інших читачів).
|
||||
waitCh := make(chan struct{}) |
||||
|
||||
task := WriteTask{ |
||||
WaitCh: waitCh, |
||||
DataPage: redo.PageToWrite{ |
||||
PageNo: newDataPage.PageNo, |
||||
Data: newDataPage.Data, |
||||
}, |
||||
IndexPages: redoWriter.IndexPagesToWrite(), |
||||
} |
||||
|
||||
s.appendWriteTaskToQueue(task) |
||||
|
||||
<-waitCh |
||||
|
||||
for _, pageNo := range dataPagesToRelease { |
||||
s.releaseDataPage(pageNo) |
||||
} |
||||
for _, pageNo := range indexPagesToRelease { |
||||
s.releaseIndexPage(pageNo) |
||||
} |
||||
return redoWriter.GetReport(), nil |
||||
} |
||||
|
||||
// DELETE
|
||||
|
||||
type PageLists struct { |
||||
DataPages []uint32 |
||||
IndexPages []uint32 |
||||
} |
||||
|
||||
type Level struct { |
||||
PageNo uint32 |
||||
PageData []byte |
||||
Idx int |
||||
ChildQty int |
||||
} |
||||
|
||||
func (s *Atree) GetAllPages(rootPageNo uint32) (_ PageLists, err error) { |
||||
var ( |
||||
dataPages []uint32 |
||||
indexPages []uint32 |
||||
levels []*Level |
||||
) |
||||
|
||||
buf, err := s.fetchIndexPage(rootPageNo) |
||||
if err != nil { |
||||
err = fmt.Errorf("fetchIndexPage(%d): %s", rootPageNo, err) |
||||
return |
||||
} |
||||
indexPages = append(indexPages, rootPageNo) |
||||
|
||||
if buf[isDataPageNumbersIdx] == 1 { |
||||
pageNumbers := listPageNumbers(buf) |
||||
dataPages = append(dataPages, pageNumbers...) |
||||
|
||||
s.releaseIndexPage(rootPageNo) |
||||
|
||||
return PageLists{ |
||||
DataPages: dataPages, |
||||
IndexPages: indexPages, |
||||
}, nil |
||||
} |
||||
|
||||
levels = append(levels, &Level{ |
||||
PageNo: rootPageNo, |
||||
PageData: buf, |
||||
Idx: 0, |
||||
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]), |
||||
}) |
||||
|
||||
for { |
||||
if len(levels) == 0 { |
||||
return PageLists{ |
||||
DataPages: dataPages, |
||||
IndexPages: indexPages, |
||||
}, nil |
||||
} |
||||
|
||||
lastIdx := len(levels) - 1 |
||||
level := levels[lastIdx] |
||||
|
||||
if level.Idx < level.ChildQty { |
||||
pageNo := getPageNo(level.PageData, level.Idx) |
||||
level.Idx++ |
||||
|
||||
var buf []byte |
||||
buf, err = s.fetchIndexPage(pageNo) |
||||
if err != nil { |
||||
err = fmt.Errorf("fetchIndexPage(%d): %s", pageNo, err) |
||||
return |
||||
} |
||||
indexPages = append(indexPages, pageNo) |
||||
|
||||
if buf[isDataPageNumbersIdx] == 1 { |
||||
pageNumbers := listPageNumbers(buf) |
||||
dataPages = append(dataPages, pageNumbers...) |
||||
|
||||
s.releaseIndexPage(pageNo) |
||||
} else { |
||||
levels = append(levels, &Level{ |
||||
PageNo: pageNo, |
||||
PageData: buf, |
||||
Idx: 0, |
||||
ChildQty: bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]), |
||||
}) |
||||
} |
||||
} else { |
||||
s.releaseIndexPage(level.PageNo) |
||||
levels = levels[:lastIdx] |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,187 @@ |
||||
package atree |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/enc" |
||||
) |
||||
|
||||
type BackwardCursor struct { |
||||
metricType octopus.MetricType |
||||
fracDigits byte |
||||
atree *Atree |
||||
pageNo uint32 |
||||
pageData []byte |
||||
timestampDecompressor octopus.TimestampDecompressor |
||||
valueDecompressor octopus.ValueDecompressor |
||||
} |
||||
|
||||
type BackwardCursorOptions struct { |
||||
MetricType octopus.MetricType |
||||
FracDigits byte |
||||
PageNo uint32 |
||||
PageData []byte |
||||
Atree *Atree |
||||
} |
||||
|
||||
func NewBackwardCursor(opt BackwardCursorOptions) (*BackwardCursor, error) { |
||||
switch opt.MetricType { |
||||
case octopus.Instant, octopus.Cumulative: |
||||
// ok
|
||||
default: |
||||
return nil, fmt.Errorf("MetricType option has wrong value: %d", opt.MetricType) |
||||
} |
||||
if opt.FracDigits > octopus.MaxFracDigits { |
||||
return nil, errors.New("FracDigits option is required") |
||||
} |
||||
if opt.Atree == nil { |
||||
return nil, errors.New("Atree option is required") |
||||
} |
||||
if opt.PageNo == 0 { |
||||
return nil, errors.New("PageNo option is required") |
||||
} |
||||
if len(opt.PageData) == 0 { |
||||
return nil, errors.New("PageData option is required") |
||||
} |
||||
|
||||
s := &BackwardCursor{ |
||||
metricType: opt.MetricType, |
||||
fracDigits: opt.FracDigits, |
||||
atree: opt.Atree, |
||||
pageNo: opt.PageNo, |
||||
pageData: opt.PageData, |
||||
} |
||||
err := s.makeDecompressors() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return s, nil |
||||
} |
||||
|
||||
// timestamp, value, done, error
|
||||
func (s *BackwardCursor) Prev() (uint32, float64, bool, error) { |
||||
var ( |
||||
timestamp uint32 |
||||
value float64 |
||||
done bool |
||||
err error |
||||
) |
||||
|
||||
timestamp, done = s.timestampDecompressor.NextValue() |
||||
if !done { |
||||
value, done = s.valueDecompressor.NextValue() |
||||
if done { |
||||
return 0, 0, false, |
||||
fmt.Errorf("corrupted data page %d: has timestamp, no value", |
||||
s.pageNo) |
||||
} |
||||
return timestamp, value, false, nil |
||||
} |
||||
|
||||
prevPageNo := bin.GetUint32(s.pageData[prevPageIdx:]) |
||||
if prevPageNo == 0 { |
||||
return 0, 0, true, nil |
||||
} |
||||
s.atree.releaseDataPage(s.pageNo) |
||||
|
||||
s.pageNo = prevPageNo |
||||
s.pageData, err = s.atree.fetchDataPage(s.pageNo) |
||||
if err != nil { |
||||
return 0, 0, false, fmt.Errorf("atree.fetchDataPage(%d): %s", s.pageNo, err) |
||||
} |
||||
|
||||
err = s.makeDecompressors() |
||||
if err != nil { |
||||
return 0, 0, false, err |
||||
} |
||||
|
||||
timestamp, done = s.timestampDecompressor.NextValue() |
||||
if done { |
||||
return 0, 0, false, |
||||
fmt.Errorf("corrupted data page %d: no timestamps", |
||||
s.pageNo) |
||||
} |
||||
value, done = s.valueDecompressor.NextValue() |
||||
if done { |
||||
return 0, 0, false, |
||||
fmt.Errorf("corrupted data page %d: no values", |
||||
s.pageNo) |
||||
} |
||||
return timestamp, value, false, nil |
||||
} |
||||
|
||||
func (s *BackwardCursor) Close() { |
||||
s.atree.releaseDataPage(s.pageNo) |
||||
} |
||||
|
||||
// HELPER
|
||||
|
||||
func (s *BackwardCursor) makeDecompressors() error { |
||||
timestampsPayloadSize := bin.GetUint16(s.pageData[timestampsSizeIdx:]) |
||||
valuesPayloadSize := bin.GetUint16(s.pageData[valuesSizeIdx:]) |
||||
|
||||
payloadSize := timestampsPayloadSize + valuesPayloadSize |
||||
|
||||
if payloadSize > dataFooterIdx { |
||||
return fmt.Errorf("corrupted data page %d: timestamps + values size %d gt payload size", |
||||
s.pageNo, payloadSize) |
||||
} |
||||
|
||||
s.timestampDecompressor = enc.NewReverseTimeDeltaOfDeltaDecompressor( |
||||
s.pageData[:timestampsPayloadSize], |
||||
) |
||||
|
||||
vbuf := s.pageData[timestampsPayloadSize : timestampsPayloadSize+valuesPayloadSize] |
||||
|
||||
switch s.metricType { |
||||
case octopus.Instant: |
||||
s.valueDecompressor = enc.NewReverseInstantDeltaDecompressor( |
||||
vbuf, s.fracDigits) |
||||
|
||||
case octopus.Cumulative: |
||||
s.valueDecompressor = enc.NewReverseCumulativeDeltaDecompressor( |
||||
vbuf, s.fracDigits) |
||||
|
||||
default: |
||||
return fmt.Errorf("bug: wrong metricType %d", s.metricType) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func makeDecompressors(pageData []byte, metricType octopus.MetricType, fracDigits byte) ( |
||||
octopus.TimestampDecompressor, octopus.ValueDecompressor, error, |
||||
) { |
||||
timestampsPayloadSize := bin.GetUint16(pageData[timestampsSizeIdx:]) |
||||
valuesPayloadSize := bin.GetUint16(pageData[valuesSizeIdx:]) |
||||
|
||||
payloadSize := timestampsPayloadSize + valuesPayloadSize |
||||
|
||||
if payloadSize > dataFooterIdx { |
||||
return nil, nil, fmt.Errorf("corrupted: timestamps + values size %d > payload size", |
||||
payloadSize) |
||||
} |
||||
|
||||
timestampDecompressor := enc.NewReverseTimeDeltaOfDeltaDecompressor( |
||||
pageData[:timestampsPayloadSize], |
||||
) |
||||
|
||||
vbuf := pageData[timestampsPayloadSize : timestampsPayloadSize+valuesPayloadSize] |
||||
|
||||
var valueDecompressor octopus.ValueDecompressor |
||||
switch metricType { |
||||
case octopus.Instant: |
||||
valueDecompressor = enc.NewReverseInstantDeltaDecompressor( |
||||
vbuf, fracDigits) |
||||
|
||||
case octopus.Cumulative: |
||||
valueDecompressor = enc.NewReverseCumulativeDeltaDecompressor( |
||||
vbuf, fracDigits) |
||||
|
||||
default: |
||||
return nil, nil, fmt.Errorf("bug: wrong metricType %d", metricType) |
||||
} |
||||
return timestampDecompressor, valueDecompressor, nil |
||||
} |
@ -0,0 +1,430 @@ |
||||
package atree |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"hash/crc32" |
||||
"io/fs" |
||||
"math" |
||||
"os" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/atree/redo" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
type AllocatedPage struct { |
||||
PageNo uint32 |
||||
Data []byte |
||||
IsReused bool |
||||
} |
||||
|
||||
type readResult struct { |
||||
Data []byte |
||||
Err error |
||||
} |
||||
|
||||
// INDEX PAGES
|
||||
|
||||
func (s *Atree) DeleteIndexPages(pageNumbers []uint32) { |
||||
s.mutex.Lock() |
||||
for _, pageNo := range pageNumbers { |
||||
delete(s.indexPages, pageNo) |
||||
} |
||||
s.mutex.Unlock() |
||||
} |
||||
|
||||
func (s *Atree) fetchIndexPage(pageNo uint32) ([]byte, error) { |
||||
s.mutex.Lock() |
||||
p, ok := s.indexPages[pageNo] |
||||
if ok { |
||||
p.ReferenceCount++ |
||||
s.mutex.Unlock() |
||||
return p.Buf, nil |
||||
} |
||||
|
||||
resultCh := make(chan readResult, 1) |
||||
s.indexWaits[pageNo] = append(s.indexWaits[pageNo], resultCh) |
||||
if len(s.indexWaits[pageNo]) == 1 { |
||||
s.indexPagesToRead = append(s.indexPagesToRead, pageNo) |
||||
s.mutex.Unlock() |
||||
|
||||
select { |
||||
case s.readSignalCh <- struct{}{}: |
||||
default: |
||||
} |
||||
} else { |
||||
s.mutex.Unlock() |
||||
} |
||||
|
||||
result := <-resultCh |
||||
if result.Err == nil { |
||||
result.Err = s.verifyCRC(result.Data, IndexPageSize) |
||||
} |
||||
return result.Data, result.Err |
||||
} |
||||
|
||||
func (s *Atree) releaseIndexPage(pageNo uint32) { |
||||
s.mutex.Lock() |
||||
defer s.mutex.Unlock() |
||||
|
||||
p, ok := s.indexPages[pageNo] |
||||
if ok { |
||||
if p.ReferenceCount > 0 { |
||||
p.ReferenceCount-- |
||||
return |
||||
} else { |
||||
octopus.Abort( |
||||
octopus.ReferenceCountBug, |
||||
fmt.Errorf("call releaseIndexPage on page %d with reference count = %d", |
||||
pageNo, p.ReferenceCount), |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Atree) allocIndexPage() AllocatedPage { |
||||
var ( |
||||
allocated = AllocatedPage{ |
||||
Data: make([]byte, IndexPageSize), |
||||
} |
||||
) |
||||
|
||||
allocated.PageNo = s.indexFreelist.ReservePage() |
||||
if allocated.PageNo > 0 { |
||||
allocated.IsReused = true |
||||
|
||||
s.mutex.Lock() |
||||
} else { |
||||
s.mutex.Lock() |
||||
if s.allocatedIndexPagesQty == math.MaxUint32 { |
||||
octopus.Abort(octopus.MaxAtreeSizeExceeded, |
||||
errors.New("no space in Atree index")) |
||||
} |
||||
s.allocatedIndexPagesQty++ |
||||
allocated.PageNo = s.allocatedIndexPagesQty |
||||
} |
||||
|
||||
s.indexPages[allocated.PageNo] = &_page{ |
||||
PageNo: allocated.PageNo, |
||||
Buf: allocated.Data, |
||||
ReferenceCount: 1, |
||||
} |
||||
s.mutex.Unlock() |
||||
return allocated |
||||
} |
||||
|
||||
// DATA PAGES
|
||||
|
||||
func (s *Atree) DeleteDataPages(pageNumbers []uint32) { |
||||
s.mutex.Lock() |
||||
for _, pageNo := range pageNumbers { |
||||
delete(s.dataPages, pageNo) |
||||
} |
||||
s.mutex.Unlock() |
||||
} |
||||
|
||||
func (s *Atree) fetchDataPage(pageNo uint32) ([]byte, error) { |
||||
s.mutex.Lock() |
||||
p, ok := s.dataPages[pageNo] |
||||
if ok { |
||||
p.ReferenceCount++ |
||||
s.mutex.Unlock() |
||||
return p.Buf, nil |
||||
} |
||||
|
||||
resultCh := make(chan readResult, 1) |
||||
s.dataWaits[pageNo] = append(s.dataWaits[pageNo], resultCh) |
||||
if len(s.dataWaits[pageNo]) == 1 { |
||||
s.dataPagesToRead = append(s.dataPagesToRead, pageNo) |
||||
s.mutex.Unlock() |
||||
|
||||
select { |
||||
case s.readSignalCh <- struct{}{}: |
||||
default: |
||||
} |
||||
} else { |
||||
s.mutex.Unlock() |
||||
} |
||||
result := <-resultCh |
||||
if result.Err == nil { |
||||
result.Err = s.verifyCRC(result.Data, DataPageSize) |
||||
} |
||||
return result.Data, result.Err |
||||
} |
||||
|
||||
func (s *Atree) releaseDataPage(pageNo uint32) { |
||||
s.mutex.Lock() |
||||
defer s.mutex.Unlock() |
||||
|
||||
p, ok := s.dataPages[pageNo] |
||||
if ok { |
||||
if p.ReferenceCount > 0 { |
||||
p.ReferenceCount-- |
||||
return |
||||
} else { |
||||
octopus.Abort( |
||||
octopus.ReferenceCountBug, |
||||
fmt.Errorf("call releaseDataPage on page %d with reference count = %d", |
||||
pageNo, p.ReferenceCount), |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Atree) allocDataPage() AllocatedPage { |
||||
var ( |
||||
allocated = AllocatedPage{ |
||||
Data: make([]byte, DataPageSize), |
||||
} |
||||
) |
||||
|
||||
allocated.PageNo = s.dataFreelist.ReservePage() |
||||
if allocated.PageNo > 0 { |
||||
allocated.IsReused = true |
||||
s.mutex.Lock() |
||||
} else { |
||||
s.mutex.Lock() |
||||
if s.allocatedDataPagesQty == math.MaxUint32 { |
||||
octopus.Abort(octopus.MaxAtreeSizeExceeded, |
||||
errors.New("no space in Atree index")) |
||||
} |
||||
s.allocatedDataPagesQty++ |
||||
allocated.PageNo = s.allocatedDataPagesQty |
||||
} |
||||
|
||||
s.dataPages[allocated.PageNo] = &_page{ |
||||
PageNo: allocated.PageNo, |
||||
Buf: allocated.Data, |
||||
ReferenceCount: 1, |
||||
} |
||||
s.mutex.Unlock() |
||||
return allocated |
||||
} |
||||
|
||||
// READ
|
||||
|
||||
func (s *Atree) pageReader() { |
||||
for { |
||||
select { |
||||
case <-s.readSignalCh: |
||||
s.readPages() |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Atree) readPages() { |
||||
s.mutex.Lock() |
||||
if len(s.indexPagesToRead) == 0 && len(s.dataPagesToRead) == 0 { |
||||
s.mutex.Unlock() |
||||
return |
||||
} |
||||
indexPagesToRead := s.indexPagesToRead |
||||
s.indexPagesToRead = nil |
||||
dataPagesToRead := s.dataPagesToRead |
||||
s.dataPagesToRead = nil |
||||
s.mutex.Unlock() |
||||
|
||||
for _, pageNo := range dataPagesToRead { |
||||
buf := make([]byte, DataPageSize) |
||||
off := (pageNo - 1) * DataPageSize |
||||
n, err := s.dataFile.ReadAt(buf, int64(off)) |
||||
if n != DataPageSize { |
||||
err = fmt.Errorf("read %d instead of %d", n, DataPageSize) |
||||
} |
||||
|
||||
s.mutex.Lock() |
||||
resultChannels := s.dataWaits[pageNo] |
||||
delete(s.dataWaits, pageNo) |
||||
|
||||
if err != nil { |
||||
s.mutex.Unlock() |
||||
for _, resultCh := range resultChannels { |
||||
resultCh <- readResult{ |
||||
Err: err, |
||||
} |
||||
} |
||||
} else { |
||||
s.dataPages[pageNo] = &_page{ |
||||
PageNo: pageNo, |
||||
Buf: buf, |
||||
ReferenceCount: len(resultChannels), |
||||
} |
||||
s.mutex.Unlock() |
||||
for _, resultCh := range resultChannels { |
||||
resultCh <- readResult{ |
||||
Data: buf, |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
for _, pageNo := range indexPagesToRead { |
||||
buf := make([]byte, IndexPageSize) |
||||
off := (pageNo - 1) * IndexPageSize |
||||
n, err := s.indexFile.ReadAt(buf, int64(off)) |
||||
if n != IndexPageSize { |
||||
err = fmt.Errorf("read %d instead of %d", n, IndexPageSize) |
||||
} |
||||
|
||||
s.mutex.Lock() |
||||
resultChannels := s.indexWaits[pageNo] |
||||
delete(s.indexWaits, pageNo) |
||||
|
||||
if err != nil { |
||||
s.mutex.Unlock() |
||||
for _, resultCh := range resultChannels { |
||||
resultCh <- readResult{ |
||||
Err: err, |
||||
} |
||||
} |
||||
} else { |
||||
s.indexPages[pageNo] = &_page{ |
||||
PageNo: pageNo, |
||||
Buf: buf, |
||||
ReferenceCount: len(resultChannels), |
||||
} |
||||
s.mutex.Unlock() |
||||
for _, resultCh := range resultChannels { |
||||
resultCh <- readResult{ |
||||
Data: buf, |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
} |
||||
|
||||
// WRITE
|
||||
|
||||
func (s *Atree) pageWriter() { |
||||
for { |
||||
select { |
||||
case <-s.writeSignalCh: |
||||
err := s.writeTasks() |
||||
if err != nil { |
||||
octopus.Abort(octopus.WriteToAtreeFailed, err) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
type WriteTask struct { |
||||
WaitCh chan struct{} |
||||
DataPage redo.PageToWrite |
||||
IndexPages []redo.PageToWrite |
||||
} |
||||
|
||||
func (s *Atree) appendWriteTaskToQueue(task WriteTask) { |
||||
s.mutex.Lock() |
||||
s.writeTasksQueue = append(s.writeTasksQueue, task) |
||||
s.mutex.Unlock() |
||||
|
||||
select { |
||||
case s.writeSignalCh <- struct{}{}: |
||||
default: |
||||
} |
||||
} |
||||
|
||||
func (s *Atree) writeTasks() error { |
||||
s.mutex.Lock() |
||||
tasks := s.writeTasksQueue |
||||
s.writeTasksQueue = nil |
||||
s.mutex.Unlock() |
||||
|
||||
for _, task := range tasks { |
||||
// data page
|
||||
p := task.DataPage |
||||
if len(p.Data) != DataPageSize { |
||||
return fmt.Errorf("wrong data page %d size: %d", |
||||
p.PageNo, len(p.Data)) |
||||
} |
||||
off := (p.PageNo - 1) * DataPageSize |
||||
n, err := s.dataFile.WriteAt(p.Data, int64(off)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if n != len(p.Data) { |
||||
return fmt.Errorf("write %d instead of %d", n, len(p.Data)) |
||||
} |
||||
|
||||
// index pages
|
||||
for _, p := range task.IndexPages { |
||||
if len(p.Data) != IndexPageSize { |
||||
return fmt.Errorf("wrong index page %d size: %d", |
||||
p.PageNo, len(p.Data)) |
||||
} |
||||
bin.PutUint32(p.Data[indexCRC32Idx:], crc32.ChecksumIEEE(p.Data[:indexCRC32Idx])) |
||||
|
||||
off := (p.PageNo - 1) * IndexPageSize |
||||
n, err := s.indexFile.WriteAt(p.Data, int64(off)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if n != len(p.Data) { |
||||
return fmt.Errorf("write %d instead of %d", n, len(p.Data)) |
||||
} |
||||
} |
||||
close(task.WaitCh) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// IO
|
||||
|
||||
func isFileExist(fileName string) (bool, error) { |
||||
_, err := os.Stat(fileName) |
||||
if err != nil { |
||||
if errors.Is(err, fs.ErrNotExist) { |
||||
return false, nil |
||||
} else { |
||||
return false, err |
||||
} |
||||
} else { |
||||
return true, nil |
||||
} |
||||
} |
||||
|
||||
func openFile(fileName string, pageSize int) (_ *os.File, _ uint32, err error) { |
||||
file, err := os.OpenFile(fileName, os.O_RDWR, filePerm) |
||||
if err != nil { |
||||
return |
||||
} |
||||
fi, err := file.Stat() |
||||
if err != nil { |
||||
return |
||||
} |
||||
fileSize := fi.Size() |
||||
|
||||
if (fileSize % int64(pageSize)) > 0 { |
||||
err = fmt.Errorf("the file size %d is not a multiple of the page size %d", |
||||
fileSize, pageSize) |
||||
return |
||||
} |
||||
|
||||
allocatedPagesQty := fileSize / int64(pageSize) |
||||
if allocatedPagesQty > math.MaxUint32 { |
||||
err = fmt.Errorf("allocated pages %d is > max pages %d", |
||||
allocatedPagesQty, math.MaxUint32) |
||||
return |
||||
} |
||||
|
||||
return file, uint32(allocatedPagesQty), nil |
||||
} |
||||
|
||||
func (s *Atree) ApplyREDO(task WriteTask) { |
||||
s.appendWriteTaskToQueue(task) |
||||
} |
||||
|
||||
func (s *Atree) verifyCRC(data []byte, pageSize int) error { |
||||
var ( |
||||
pos = pageSize - 4 |
||||
calculatedCRC = crc32.ChecksumIEEE(data[:pos]) |
||||
storedCRC = bin.GetUint32(data[pos:]) |
||||
) |
||||
if calculatedCRC != storedCRC { |
||||
return fmt.Errorf("calculatedCRC %d not equal storedCRC %d", |
||||
calculatedCRC, storedCRC) |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,214 @@ |
||||
package atree |
||||
|
||||
import ( |
||||
"hash/crc32" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
type ValueAtComparator struct { |
||||
buf []byte |
||||
timestamp uint32 |
||||
} |
||||
|
||||
func (s ValueAtComparator) CompareTo(elemIdx int) int { |
||||
var ( |
||||
pos = elemIdx * timestampSize |
||||
elem = bin.GetUint32(s.buf[pos:]) |
||||
) |
||||
|
||||
if s.timestamp < elem { |
||||
return -1 |
||||
} else if s.timestamp > elem { |
||||
return 1 |
||||
} else { |
||||
return 0 |
||||
} |
||||
} |
||||
|
||||
func BinarySearch(qty int, keyComparator bin.KeyComparator) (elemIdx int, isFound bool) { |
||||
if qty == 0 { |
||||
return |
||||
} |
||||
a := 0 |
||||
b := qty - 1 |
||||
for { |
||||
var ( |
||||
elemIdx = (b-a)/2 + a |
||||
code = keyComparator.CompareTo(elemIdx) |
||||
) |
||||
if code == 1 { |
||||
a = elemIdx + 1 |
||||
if a > b { |
||||
return elemIdx, false // +1
|
||||
} |
||||
} else if code == -1 { |
||||
b = elemIdx - 1 |
||||
if b < a { |
||||
if elemIdx == 0 { |
||||
return 0, false |
||||
} else { |
||||
return elemIdx - 1, false |
||||
} |
||||
} |
||||
} else { |
||||
return elemIdx, true |
||||
} |
||||
} |
||||
} |
||||
|
||||
type chunksToDataPageReq struct { |
||||
PrevPageNo uint32 |
||||
TimestampsChunks [][]byte |
||||
TimestampsSize uint16 |
||||
ValuesChunks [][]byte |
||||
ValuesSize uint16 |
||||
} |
||||
|
||||
func chunksToDataPage(buf []byte, req chunksToDataPageReq) { |
||||
bin.PutUint32(buf[prevPageIdx:], req.PrevPageNo) |
||||
bin.PutUint16(buf[timestampsSizeIdx:], req.TimestampsSize) |
||||
bin.PutUint16(buf[valuesSizeIdx:], req.ValuesSize) |
||||
|
||||
var ( |
||||
remainingSize = int(req.TimestampsSize) |
||||
pos = 0 |
||||
) |
||||
for _, chunk := range req.TimestampsChunks { |
||||
if remainingSize >= len(chunk) { |
||||
copy(buf[pos:], chunk) |
||||
remainingSize -= len(chunk) |
||||
pos += len(chunk) |
||||
} else { |
||||
copy(buf[pos:], chunk[:remainingSize]) |
||||
break |
||||
} |
||||
} |
||||
|
||||
remainingSize = int(req.ValuesSize) |
||||
pos = int(req.TimestampsSize) |
||||
|
||||
for _, chunk := range req.ValuesChunks { |
||||
if remainingSize >= len(chunk) { |
||||
copy(buf[pos:], chunk) |
||||
remainingSize -= len(chunk) |
||||
pos += len(chunk) |
||||
} else { |
||||
copy(buf[pos:], chunk[:remainingSize]) |
||||
break |
||||
} |
||||
} |
||||
bin.PutUint32(buf[dataCRC32Idx:], crc32.ChecksumIEEE(buf[:dataCRC32Idx])) |
||||
} |
||||
|
||||
func setPrevPageNo(buf []byte, pageNo uint32) { |
||||
bin.PutUint32(buf[prevPageIdx:], pageNo) |
||||
} |
||||
|
||||
func getPrevPageNo(buf []byte) uint32 { |
||||
return bin.GetUint32(buf[prevPageIdx:]) |
||||
} |
||||
|
||||
func findPageNo(buf []byte, timestamp uint32) (pageNo uint32) { |
||||
var ( |
||||
qty = bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]) |
||||
comparator = ValueAtComparator{ |
||||
buf: buf, |
||||
timestamp: timestamp, |
||||
} |
||||
) |
||||
elemIdx, _ := BinarySearch(qty, comparator) |
||||
pos := indexFooterIdx - (elemIdx+1)*PageNoSize |
||||
return bin.GetUint32(buf[pos:]) |
||||
} |
||||
|
||||
func findPageNoIdx(buf []byte, timestamp uint32) (idx int) { |
||||
var ( |
||||
qty = bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]) |
||||
comparator = ValueAtComparator{ |
||||
buf: buf, |
||||
timestamp: timestamp, |
||||
} |
||||
) |
||||
elemIdx, _ := BinarySearch(qty, comparator) |
||||
return elemIdx |
||||
} |
||||
|
||||
func findPageNoForDeleteSince(buf []byte, since uint32) (uint32, int, int) { |
||||
var ( |
||||
qty = bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]) |
||||
comparator = ValueAtComparator{ |
||||
buf: buf, |
||||
timestamp: since, |
||||
} |
||||
) |
||||
elemIdx, _ := BinarySearch(qty, comparator) |
||||
pos := elemIdx * timestampSize |
||||
timestamp := bin.GetUint32(buf[pos:]) |
||||
|
||||
if timestamp == since { |
||||
if elemIdx == 0 { |
||||
return 0, qty, -1 |
||||
} |
||||
elemIdx-- |
||||
} |
||||
pos = indexFooterIdx - (elemIdx+1)*PageNoSize |
||||
return bin.GetUint32(buf[pos:]), qty, elemIdx |
||||
} |
||||
|
||||
func getLastPageNo(buf []byte) (pageNo uint32) { |
||||
qty := bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]) |
||||
pos := indexFooterIdx - qty*PageNoSize |
||||
return bin.GetUint32(buf[pos:]) |
||||
} |
||||
|
||||
func getPageNo(buf []byte, idx int) (pageNo uint32) { |
||||
pos := indexFooterIdx - (idx+1)*PageNoSize |
||||
return bin.GetUint32(buf[pos:]) |
||||
} |
||||
|
||||
func listPageNumbers(buf []byte) (pageNumbers []uint32) { |
||||
qty := bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]) |
||||
pos := indexFooterIdx - PageNoSize |
||||
for range qty { |
||||
pageNumbers = append(pageNumbers, bin.GetUint32(buf[pos:])) |
||||
pos -= PageNoSize |
||||
} |
||||
return |
||||
} |
||||
|
||||
// include since timestamp
|
||||
func listPageNumbersSince(buf []byte, timestamp uint32) (pageNumbers []uint32) { |
||||
var ( |
||||
qty = bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]) |
||||
comparator = ValueAtComparator{ |
||||
buf: buf, |
||||
timestamp: timestamp, |
||||
} |
||||
) |
||||
elemIdx, _ := BinarySearch(qty, comparator) |
||||
pos := indexFooterIdx - (elemIdx+1)*PageNoSize |
||||
for range qty { |
||||
pageNumbers = append(pageNumbers, bin.GetUint32(buf[pos:])) |
||||
pos -= PageNoSize |
||||
} |
||||
return |
||||
} |
||||
|
||||
func getSince(buf []byte) uint32 { |
||||
return bin.GetUint32(buf[0:]) |
||||
} |
||||
|
||||
func appendPair(buf []byte, timestamp uint32, pageNo uint32) bool { |
||||
qty := bin.GetUint16AsInt(buf[indexRecordsQtyIdx:]) |
||||
free := indexFooterIdx - qty*pairSize |
||||
if free < pairSize { |
||||
return false |
||||
} |
||||
pos := qty * timestampSize |
||||
bin.PutUint32(buf[pos:], timestamp) |
||||
pos = indexFooterIdx - (qty+1)*PageNoSize |
||||
bin.PutUint32(buf[pos:], pageNo) |
||||
bin.PutIntAsUint16(buf[indexRecordsQtyIdx:], qty+1) |
||||
return true |
||||
} |
@ -0,0 +1,96 @@ |
||||
package redo |
||||
|
||||
import ( |
||||
"fmt" |
||||
"hash/crc32" |
||||
"io" |
||||
"os" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
type REDOFile struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
IsDataPageReused bool |
||||
DataPage PageToWrite |
||||
IsRootChanged bool |
||||
RootPageNo uint32 |
||||
ReusedIndexPages []uint32 |
||||
IndexPages []PageToWrite |
||||
} |
||||
|
||||
type ReadREDOFileReq struct { |
||||
FileName string |
||||
DataPageSize int |
||||
IndexPageSize int |
||||
} |
||||
|
||||
func ReadREDOFile(req ReadREDOFileReq) (*REDOFile, error) { |
||||
buf, err := os.ReadFile(req.FileName) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if len(buf) < 25 { |
||||
return nil, io.EOF |
||||
} |
||||
|
||||
var ( |
||||
end = len(buf) - 4 |
||||
payload = buf[:end] |
||||
checksum = bin.GetUint32(buf[end:]) |
||||
calculatedChecksum = crc32.ChecksumIEEE(payload) |
||||
) |
||||
|
||||
// Помилка чексуми означає що файл або недописаний, або пошкодженний
|
||||
if checksum != calculatedChecksum { |
||||
return nil, fmt.Errorf("written checksum %d not equal calculated checksum %d", |
||||
checksum, calculatedChecksum) |
||||
} |
||||
|
||||
var ( |
||||
redoLog = REDOFile{ |
||||
MetricID: bin.GetUint32(buf[0:]), |
||||
Timestamp: bin.GetUint32(buf[4:]), |
||||
Value: bin.GetFloat64(buf[8:]), |
||||
IsDataPageReused: buf[16] == 1, |
||||
DataPage: PageToWrite{ |
||||
PageNo: bin.GetUint32(buf[17:]), |
||||
Data: buf[21 : 21+req.DataPageSize], |
||||
}, |
||||
} |
||||
pos = 21 + req.DataPageSize |
||||
) |
||||
|
||||
for { |
||||
if pos == len(payload) { |
||||
return &redoLog, nil |
||||
} |
||||
|
||||
if pos > len(payload) { |
||||
return nil, io.EOF |
||||
} |
||||
|
||||
flags := buf[pos] |
||||
|
||||
item := PageToWrite{ |
||||
PageNo: bin.GetUint32(buf[pos+1:]), |
||||
} |
||||
pos += 5 // flags + pageNo
|
||||
item.Data = buf[pos : pos+req.IndexPageSize] |
||||
pos += req.IndexPageSize |
||||
|
||||
redoLog.IndexPages = append(redoLog.IndexPages, item) |
||||
|
||||
if (flags & FlagReused) == FlagReused { |
||||
redoLog.ReusedIndexPages = append(redoLog.ReusedIndexPages, item.PageNo) |
||||
} |
||||
|
||||
if (flags & FlagNewRoot) == FlagNewRoot { |
||||
redoLog.IsRootChanged = true |
||||
redoLog.RootPageNo = item.PageNo |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,207 @@ |
||||
package redo |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"hash" |
||||
"hash/crc32" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
const ( |
||||
FlagReused byte = 1 // сторінка із FreeList
|
||||
FlagNewRoot byte = 2 // новая страница
|
||||
) |
||||
|
||||
type PageToWrite struct { |
||||
PageNo uint32 |
||||
Data []byte |
||||
} |
||||
|
||||
type Writer struct { |
||||
metricID uint32 |
||||
timestamp uint32 |
||||
value float64 |
||||
tmp []byte |
||||
fileName string |
||||
file *os.File |
||||
hasher hash.Hash32 |
||||
isDataPageReused bool |
||||
dataPageNo uint32 |
||||
isRootChanged bool |
||||
newRootPageNo uint32 |
||||
indexPages []uint32 |
||||
reusedIndexPages []uint32 |
||||
indexPagesToWrite []PageToWrite |
||||
} |
||||
|
||||
type WriterOptions struct { |
||||
Dir string |
||||
MetricID uint32 |
||||
Value float64 |
||||
Timestamp uint32 |
||||
IsDataPageReused bool |
||||
DataPageNo uint32 |
||||
Page []byte |
||||
} |
||||
|
||||
// dataPage можно записати 1 раз. Щоб не заплутувати інтерфейс - передаю data сторінку
|
||||
// через Options. Index сторінок може бути від 1 до N, тому виділяю окремий метод
|
||||
func NewWriter(opt WriterOptions) (*Writer, error) { |
||||
if opt.Dir == "" { |
||||
return nil, errors.New("Dir option is required") |
||||
} |
||||
if opt.MetricID == 0 { |
||||
return nil, errors.New("MetricID option is required") |
||||
} |
||||
if opt.DataPageNo == 0 { |
||||
return nil, errors.New("DataPageNo option is required") |
||||
} |
||||
// if len(opt.Page) != octopus.DataPageSize {
|
||||
// return nil, fmt.Errorf("bug: wrong data page size %d", len(opt.Page))
|
||||
// }
|
||||
|
||||
s := &Writer{ |
||||
fileName: JoinREDOFileName(opt.Dir, opt.MetricID), |
||||
metricID: opt.MetricID, |
||||
timestamp: opt.Timestamp, |
||||
value: opt.Value, |
||||
tmp: make([]byte, 21), |
||||
isDataPageReused: opt.IsDataPageReused, |
||||
dataPageNo: opt.DataPageNo, |
||||
hasher: crc32.NewIEEE(), |
||||
} |
||||
|
||||
var err error |
||||
s.file, err = os.OpenFile(s.fileName, os.O_CREATE|os.O_WRONLY, 0770) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
err = s.init(opt.Page) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return s, nil |
||||
} |
||||
|
||||
/* |
||||
Формат: |
||||
4b metricID |
||||
8b value |
||||
4b timestamp |
||||
1b flags (reused) |
||||
4b dataPageNo |
||||
8KB dataPage |
||||
*/ |
||||
func (s *Writer) init(dataPage []byte) error { |
||||
bin.PutUint32(s.tmp[0:], s.metricID) |
||||
bin.PutUint32(s.tmp[4:], s.timestamp) |
||||
bin.PutFloat64(s.tmp[8:], s.value) |
||||
if s.isDataPageReused { |
||||
s.tmp[16] = 1 |
||||
} |
||||
bin.PutUint32(s.tmp[17:], s.dataPageNo) |
||||
|
||||
_, err := s.file.Write(s.tmp) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
_, err = s.file.Write(dataPage) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.hasher.Write(s.tmp) |
||||
s.hasher.Write(dataPage) |
||||
return nil |
||||
} |
||||
|
||||
/* |
||||
Формат |
||||
1b index page flags |
||||
4b indexPageNo |
||||
Nb indexPage |
||||
*/ |
||||
func (s *Writer) AppendIndexPage(indexPageNo uint32, indexPage []byte, flags byte) error { |
||||
s.tmp[0] = flags |
||||
bin.PutUint32(s.tmp[1:], indexPageNo) |
||||
_, err := s.file.Write(s.tmp[:5]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
_, err = s.file.Write(indexPage) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.hasher.Write(s.tmp[:5]) |
||||
s.hasher.Write(indexPage) |
||||
|
||||
s.indexPages = append(s.indexPages, indexPageNo) |
||||
|
||||
if (flags & FlagReused) == FlagReused { |
||||
s.reusedIndexPages = append(s.reusedIndexPages, indexPageNo) |
||||
} |
||||
|
||||
if (flags & FlagNewRoot) == FlagNewRoot { |
||||
s.newRootPageNo = indexPageNo |
||||
s.isRootChanged = true |
||||
} |
||||
|
||||
s.indexPagesToWrite = append(s.indexPagesToWrite, |
||||
PageToWrite{ |
||||
PageNo: indexPageNo, |
||||
Data: indexPage, |
||||
}) |
||||
return nil |
||||
} |
||||
|
||||
func (s *Writer) IndexPagesToWrite() []PageToWrite { |
||||
return s.indexPagesToWrite |
||||
} |
||||
|
||||
func (s *Writer) Close() (err error) { |
||||
// финализирую запись
|
||||
bin.PutUint32(s.tmp, s.hasher.Sum32()) |
||||
_, err = s.file.Write(s.tmp[:4]) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
err = s.file.Sync() |
||||
if err != nil { |
||||
return |
||||
} |
||||
return s.file.Close() |
||||
} |
||||
|
||||
type Report struct { |
||||
FileName string |
||||
IsDataPageReused bool |
||||
DataPageNo uint32 |
||||
IsRootChanged bool |
||||
NewRootPageNo uint32 |
||||
ReusedIndexPages []uint32 |
||||
} |
||||
|
||||
func (s *Writer) GetReport() Report { |
||||
return Report{ |
||||
FileName: s.fileName, |
||||
IsDataPageReused: s.isDataPageReused, |
||||
DataPageNo: s.dataPageNo, |
||||
//IndexPages: s.indexPages,
|
||||
IsRootChanged: s.isRootChanged, |
||||
NewRootPageNo: s.newRootPageNo, |
||||
ReusedIndexPages: s.reusedIndexPages, |
||||
} |
||||
} |
||||
|
||||
// HELPERS
|
||||
|
||||
func JoinREDOFileName(dir string, metricID uint32) string { |
||||
return filepath.Join(dir, fmt.Sprintf("m%d.redo", metricID)) |
||||
} |
@ -0,0 +1,619 @@ |
||||
package atree |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
) |
||||
|
||||
type IterateAllCumulativeByTreeCursorReq struct { |
||||
FracDigits byte |
||||
PageNo uint32 |
||||
EndTimestamp uint32 |
||||
EndValue float64 |
||||
ResponseWriter *CumulativeMeasureWriter |
||||
} |
||||
|
||||
func (s *Atree) IterateAllCumulativeByTreeCursor(req IterateAllCumulativeByTreeCursorReq) error { |
||||
buf, err := s.fetchDataPage(req.PageNo) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: req.PageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Cumulative, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
var ( |
||||
endTimestamp = req.EndTimestamp |
||||
endValue = req.EndValue |
||||
) |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done { |
||||
err := req.ResponseWriter.WriteMeasure(CumulativeMeasure{ |
||||
Timestamp: endTimestamp, |
||||
Value: endValue, |
||||
Total: endValue, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
err = req.ResponseWriter.WriteMeasure(CumulativeMeasure{ |
||||
Timestamp: endTimestamp, |
||||
Value: endValue, |
||||
Total: endValue - value, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
endTimestamp = timestamp |
||||
endValue = value |
||||
} |
||||
} |
||||
|
||||
type ContinueIterateCumulativeByTreeCursorReq struct { |
||||
FracDigits byte |
||||
Since uint32 |
||||
Until uint32 |
||||
LastPageNo uint32 |
||||
EndTimestamp uint32 |
||||
EndValue float64 |
||||
ResponseWriter *CumulativeMeasureWriter |
||||
} |
||||
|
||||
func (s *Atree) ContinueIterateCumulativeByTreeCursor(req ContinueIterateCumulativeByTreeCursorReq) error { |
||||
buf, err := s.fetchDataPage(req.LastPageNo) |
||||
if err != nil { |
||||
return fmt.Errorf("fetchDataPage(%d): %s", req.LastPageNo, err) |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: req.LastPageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Cumulative, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
var ( |
||||
endTimestamp = req.EndTimestamp |
||||
endValue = req.EndValue |
||||
) |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done { |
||||
err := req.ResponseWriter.WriteMeasure(CumulativeMeasure{ |
||||
Timestamp: endTimestamp, |
||||
Value: endValue, |
||||
Total: endValue, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
if timestamp <= req.Until { |
||||
err := req.ResponseWriter.WriteMeasure(CumulativeMeasure{ |
||||
Timestamp: endTimestamp, |
||||
Value: endValue, |
||||
Total: endValue - value, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if timestamp < req.Since { |
||||
return nil |
||||
} |
||||
} else { |
||||
// bug panic
|
||||
panic("continue cumulative but timestamp > req.Until") |
||||
} |
||||
} |
||||
} |
||||
|
||||
type FindAndIterateCumulativeByTreeCursorReq struct { |
||||
FracDigits byte |
||||
Since uint32 |
||||
Until uint32 |
||||
RootPageNo uint32 |
||||
ResponseWriter *CumulativeMeasureWriter |
||||
} |
||||
|
||||
func (s *Atree) FindAndIterateCumulativeByTreeCursor(req FindAndIterateCumulativeByTreeCursorReq) error { |
||||
pageNo, buf, err := s.findDataPage(req.RootPageNo, req.Until) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: pageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Cumulative, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
var ( |
||||
endTimestamp uint32 |
||||
endValue float64 |
||||
) |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done { |
||||
if endTimestamp > 0 { |
||||
err := req.ResponseWriter.WriteMeasure(CumulativeMeasure{ |
||||
Timestamp: endTimestamp, |
||||
Value: endValue, |
||||
Total: endValue, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
if timestamp > req.Until { |
||||
continue |
||||
} |
||||
|
||||
if endTimestamp > 0 { |
||||
err := req.ResponseWriter.WriteMeasure(CumulativeMeasure{ |
||||
Timestamp: endTimestamp, |
||||
Value: endValue, |
||||
Total: endValue - value, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
endTimestamp = timestamp |
||||
endValue = value |
||||
|
||||
if timestamp < req.Since { |
||||
return nil |
||||
} |
||||
} |
||||
} |
||||
|
||||
type IterateAllInstantByTreeCursorReq struct { |
||||
FracDigits byte |
||||
PageNo uint32 |
||||
ResponseWriter *InstantMeasureWriter |
||||
} |
||||
|
||||
func (s *Atree) IterateAllInstantByTreeCursor(req IterateAllInstantByTreeCursorReq) error { |
||||
buf, err := s.fetchDataPage(req.PageNo) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: req.PageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Instant, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done { |
||||
return nil |
||||
} |
||||
|
||||
err = req.ResponseWriter.WriteMeasure(InstantMeasure{ |
||||
Timestamp: timestamp, |
||||
Value: value, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
type ContinueIterateInstantByTreeCursorReq struct { |
||||
FracDigits byte |
||||
Since uint32 |
||||
Until uint32 |
||||
LastPageNo uint32 |
||||
ResponseWriter *InstantMeasureWriter |
||||
} |
||||
|
||||
func (s *Atree) ContinueIterateInstantByTreeCursor(req ContinueIterateInstantByTreeCursorReq) error { |
||||
buf, err := s.fetchDataPage(req.LastPageNo) |
||||
if err != nil { |
||||
return fmt.Errorf("fetchDataPage(%d): %s", req.LastPageNo, err) |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: req.LastPageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Instant, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done { |
||||
// - записи закончились;
|
||||
return nil |
||||
} |
||||
|
||||
if timestamp > req.Until { |
||||
panic("continue instant timestamp > req.Until") |
||||
} |
||||
|
||||
if timestamp < req.Since { |
||||
return nil |
||||
} |
||||
|
||||
err = req.ResponseWriter.WriteMeasure(InstantMeasure{ |
||||
Timestamp: timestamp, |
||||
Value: value, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
type FindAndIterateInstantByTreeCursorReq struct { |
||||
FracDigits byte |
||||
Since uint32 |
||||
Until uint32 |
||||
RootPageNo uint32 |
||||
ResponseWriter *InstantMeasureWriter |
||||
} |
||||
|
||||
func (s *Atree) FindAndIterateInstantByTreeCursor(req FindAndIterateInstantByTreeCursorReq) error { |
||||
pageNo, buf, err := s.findDataPage(req.RootPageNo, req.Until) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: pageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Instant, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done { |
||||
return nil |
||||
} |
||||
|
||||
if timestamp > req.Until { |
||||
continue |
||||
} |
||||
|
||||
if timestamp < req.Since { |
||||
return nil |
||||
} |
||||
|
||||
err = req.ResponseWriter.WriteMeasure(InstantMeasure{ |
||||
Timestamp: timestamp, |
||||
Value: value, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
|
||||
type ContinueCollectInstantPeriodsReq struct { |
||||
FracDigits byte |
||||
Aggregator *InstantAggregator |
||||
ResponseWriter *InstantPeriodsWriter |
||||
LastPageNo uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
} |
||||
|
||||
func (s *Atree) ContinueCollectInstantPeriods(req ContinueCollectInstantPeriodsReq) error { |
||||
buf, err := s.fetchDataPage(req.LastPageNo) |
||||
if err != nil { |
||||
return fmt.Errorf("fetchDataPage(%d): %s", req.LastPageNo, err) |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: req.LastPageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Instant, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
var period InstantPeriod |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done || timestamp < req.Since { |
||||
isCompleted := req.Aggregator.FillPeriod(timestamp, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
if timestamp <= req.Until { |
||||
isCompleted := req.Aggregator.Feed(timestamp, value, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
type FindInstantPeriodsReq struct { |
||||
FracDigits byte |
||||
ResponseWriter *InstantPeriodsWriter |
||||
RootPageNo uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
GroupBy octopus.GroupBy |
||||
FirstHourOfDay int |
||||
LastDayOfMonth int |
||||
} |
||||
|
||||
func (s *Atree) FindInstantPeriods(req FindInstantPeriodsReq) error { |
||||
pageNo, buf, err := s.findDataPage(req.RootPageNo, req.Until) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
aggregator, err := NewInstantAggregator(InstantAggregatorOptions{ |
||||
GroupBy: req.GroupBy, |
||||
FirstHourOfDay: req.FirstHourOfDay, |
||||
LastDayOfMonth: req.LastDayOfMonth, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
cursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: pageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Instant, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer cursor.Close() |
||||
|
||||
var period InstantPeriod |
||||
|
||||
for { |
||||
timestamp, value, done, err := cursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done || timestamp < req.Since { |
||||
isCompleted := aggregator.FillPeriod(timestamp, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
if timestamp <= req.Until { |
||||
isCompleted := aggregator.Feed(timestamp, value, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
type FindCumulativePeriodsReq struct { |
||||
FracDigits byte |
||||
ResponseWriter *CumulativePeriodsWriter |
||||
RootPageNo uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
GroupBy octopus.GroupBy |
||||
FirstHourOfDay int |
||||
LastDayOfMonth int |
||||
} |
||||
|
||||
func (s *Atree) FindCumulativePeriods(req FindCumulativePeriodsReq) error { |
||||
pageNo, buf, err := s.findDataPage(req.RootPageNo, req.Until) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
aggregator, err := NewCumulativeAggregator(CumulativeAggregatorOptions{ |
||||
GroupBy: req.GroupBy, |
||||
FirstHourOfDay: req.FirstHourOfDay, |
||||
LastDayOfMonth: req.LastDayOfMonth, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
cursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: pageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Cumulative, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer cursor.Close() |
||||
|
||||
var period CumulativePeriod |
||||
|
||||
for { |
||||
timestamp, value, done, err := cursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done || timestamp < req.Since { |
||||
isCompleted := aggregator.FillPeriod(timestamp, value, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
if timestamp <= req.Until { |
||||
isCompleted := aggregator.Feed(timestamp, value, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
type ContinueCollectCumulativePeriodsReq struct { |
||||
FracDigits byte |
||||
Aggregator *CumulativeAggregator |
||||
ResponseWriter *CumulativePeriodsWriter |
||||
LastPageNo uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
} |
||||
|
||||
func (s *Atree) ContinueCollectCumulativePeriods(req ContinueCollectCumulativePeriodsReq) error { |
||||
buf, err := s.fetchDataPage(req.LastPageNo) |
||||
if err != nil { |
||||
return fmt.Errorf("fetchDataPage(%d): %s", req.LastPageNo, err) |
||||
} |
||||
|
||||
treeCursor, err := NewBackwardCursor(BackwardCursorOptions{ |
||||
PageNo: req.LastPageNo, |
||||
PageData: buf, |
||||
Atree: s, |
||||
FracDigits: req.FracDigits, |
||||
MetricType: octopus.Cumulative, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer treeCursor.Close() |
||||
|
||||
var period CumulativePeriod |
||||
|
||||
for { |
||||
timestamp, value, done, err := treeCursor.Prev() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if done || timestamp < req.Since { |
||||
isCompleted := req.Aggregator.FillPeriod(timestamp, value, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
if timestamp <= req.Until { |
||||
isCompleted := req.Aggregator.Feed(timestamp, value, &period) |
||||
if isCompleted { |
||||
err := req.ResponseWriter.WritePeriod(period) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,306 @@ |
||||
package atree |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"io" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/proto" |
||||
) |
||||
|
||||
// CURRENT VALUE WRITER
|
||||
|
||||
type CurrentValue struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
type CurrentValueWriter struct { |
||||
arr []byte |
||||
responder *ChunkedResponder |
||||
} |
||||
|
||||
func NewCurrentValueWriter(dst io.Writer) *CurrentValueWriter { |
||||
return &CurrentValueWriter{ |
||||
arr: make([]byte, 16), |
||||
responder: NewChunkedResponder(dst), |
||||
} |
||||
} |
||||
|
||||
func (s *CurrentValueWriter) BufferValue(m CurrentValue) { |
||||
bin.PutUint32(s.arr[0:], m.MetricID) |
||||
bin.PutUint32(s.arr[4:], m.Timestamp) |
||||
bin.PutFloat64(s.arr[8:], m.Value) |
||||
s.responder.BufferRecord(s.arr) |
||||
} |
||||
|
||||
func (s *CurrentValueWriter) Close() error { |
||||
return s.responder.Flush() |
||||
} |
||||
|
||||
// INSTANT MEASURE WRITER
|
||||
|
||||
type InstantMeasure struct { |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
type InstantMeasureWriter struct { |
||||
arr []byte |
||||
responder *ChunkedResponder |
||||
} |
||||
|
||||
func NewInstantMeasureWriter(dst io.Writer) *InstantMeasureWriter { |
||||
return &InstantMeasureWriter{ |
||||
arr: make([]byte, 12), |
||||
responder: NewChunkedResponder(dst), |
||||
} |
||||
} |
||||
|
||||
func (s *InstantMeasureWriter) BufferMeasure(m InstantMeasure) { |
||||
bin.PutUint32(s.arr[0:], m.Timestamp) |
||||
bin.PutFloat64(s.arr[4:], m.Value) |
||||
s.responder.BufferRecord(s.arr) |
||||
} |
||||
|
||||
func (s *InstantMeasureWriter) WriteMeasure(m InstantMeasure) error { |
||||
bin.PutUint32(s.arr[0:], m.Timestamp) |
||||
bin.PutFloat64(s.arr[4:], m.Value) |
||||
return s.responder.AppendRecord(s.arr) |
||||
} |
||||
|
||||
func (s *InstantMeasureWriter) Close() error { |
||||
return s.responder.Flush() |
||||
} |
||||
|
||||
// CUMULATIVE MEASURE WRITER
|
||||
|
||||
type CumulativeMeasure struct { |
||||
Timestamp uint32 |
||||
Value float64 |
||||
Total float64 |
||||
} |
||||
|
||||
type CumulativeMeasureWriter struct { |
||||
arr []byte |
||||
responder *ChunkedResponder |
||||
} |
||||
|
||||
func NewCumulativeMeasureWriter(dst io.Writer) *CumulativeMeasureWriter { |
||||
return &CumulativeMeasureWriter{ |
||||
arr: make([]byte, 20), |
||||
responder: NewChunkedResponder(dst), |
||||
} |
||||
} |
||||
|
||||
func (s *CumulativeMeasureWriter) BufferMeasure(m CumulativeMeasure) { |
||||
bin.PutUint32(s.arr[0:], m.Timestamp) |
||||
bin.PutFloat64(s.arr[4:], m.Value) |
||||
bin.PutFloat64(s.arr[12:], m.Total) |
||||
s.responder.BufferRecord(s.arr) |
||||
} |
||||
|
||||
func (s *CumulativeMeasureWriter) WriteMeasure(m CumulativeMeasure) error { |
||||
bin.PutUint32(s.arr[0:], m.Timestamp) |
||||
bin.PutFloat64(s.arr[4:], m.Value) |
||||
bin.PutFloat64(s.arr[12:], m.Total) |
||||
return s.responder.AppendRecord(s.arr) |
||||
} |
||||
|
||||
func (s *CumulativeMeasureWriter) Close() error { |
||||
return s.responder.Flush() |
||||
} |
||||
|
||||
// INSTANT AGGREGATE WRITER
|
||||
|
||||
type InstantPeriodsWriter struct { |
||||
aggregateFuncs byte |
||||
arr []byte |
||||
responder *ChunkedResponder |
||||
} |
||||
|
||||
func NewInstantPeriodsWriter(dst io.Writer, aggregateFuncs byte) *InstantPeriodsWriter { |
||||
var q int |
||||
if (aggregateFuncs & octopus.AggregateMin) == octopus.AggregateMin { |
||||
q++ |
||||
} |
||||
if (aggregateFuncs & octopus.AggregateMax) == octopus.AggregateMax { |
||||
q++ |
||||
} |
||||
if (aggregateFuncs & octopus.AggregateAvg) == octopus.AggregateAvg { |
||||
q++ |
||||
} |
||||
return &InstantPeriodsWriter{ |
||||
aggregateFuncs: aggregateFuncs, |
||||
arr: make([]byte, 12+q*8), |
||||
responder: NewChunkedResponder(dst), |
||||
} |
||||
} |
||||
|
||||
type InstantPeriod struct { |
||||
Period uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
Min float64 |
||||
Max float64 |
||||
Avg float64 |
||||
} |
||||
|
||||
func (s *InstantPeriodsWriter) BufferMeasure(p InstantPeriod) { |
||||
s.pack(p) |
||||
s.responder.BufferRecord(s.arr) |
||||
} |
||||
|
||||
func (s *InstantPeriodsWriter) WritePeriod(p InstantPeriod) error { |
||||
s.pack(p) |
||||
return s.responder.AppendRecord(s.arr) |
||||
} |
||||
|
||||
func (s *InstantPeriodsWriter) Close() error { |
||||
return s.responder.Flush() |
||||
} |
||||
|
||||
func (s *InstantPeriodsWriter) pack(p InstantPeriod) { |
||||
bin.PutUint32(s.arr[0:], p.Period) |
||||
bin.PutUint32(s.arr[4:], p.Since) |
||||
bin.PutUint32(s.arr[8:], p.Until) |
||||
|
||||
pos := 12 |
||||
if (s.aggregateFuncs & octopus.AggregateMin) == octopus.AggregateMin { |
||||
bin.PutFloat64(s.arr[pos:], p.Min) |
||||
pos += 8 |
||||
} |
||||
if (s.aggregateFuncs & octopus.AggregateMax) == octopus.AggregateMax { |
||||
bin.PutFloat64(s.arr[pos:], p.Max) |
||||
pos += 8 |
||||
} |
||||
if (s.aggregateFuncs & octopus.AggregateAvg) == octopus.AggregateAvg { |
||||
bin.PutFloat64(s.arr[pos:], p.Avg) |
||||
} |
||||
} |
||||
|
||||
// CUMULATIVE AGGREGATE WRITER
|
||||
|
||||
type CumulativePeriodsWriter struct { |
||||
arr []byte |
||||
responder *ChunkedResponder |
||||
} |
||||
|
||||
func NewCumulativePeriodsWriter(dst io.Writer) *CumulativePeriodsWriter { |
||||
return &CumulativePeriodsWriter{ |
||||
arr: make([]byte, 28), |
||||
responder: NewChunkedResponder(dst), |
||||
} |
||||
} |
||||
|
||||
type CumulativePeriod struct { |
||||
Period uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
EndValue float64 |
||||
Total float64 |
||||
} |
||||
|
||||
func (s *CumulativePeriodsWriter) BufferMeasure(p CumulativePeriod) { |
||||
s.pack(p) |
||||
s.responder.BufferRecord(s.arr) |
||||
} |
||||
|
||||
func (s *CumulativePeriodsWriter) WritePeriod(p CumulativePeriod) error { |
||||
s.pack(p) |
||||
return s.responder.AppendRecord(s.arr) |
||||
} |
||||
|
||||
func (s *CumulativePeriodsWriter) Close() error { |
||||
return s.responder.Flush() |
||||
} |
||||
|
||||
func (s *CumulativePeriodsWriter) pack(p CumulativePeriod) { |
||||
bin.PutUint32(s.arr[0:], p.Period) |
||||
bin.PutUint32(s.arr[4:], p.Since) |
||||
bin.PutUint32(s.arr[8:], p.Until) |
||||
bin.PutFloat64(s.arr[12:], p.EndValue) |
||||
bin.PutFloat64(s.arr[20:], p.Total) |
||||
} |
||||
|
||||
// CHUNKED RESPONDER
|
||||
|
||||
//const headerSize = 3
|
||||
|
||||
var endMsg = []byte{ |
||||
proto.RespEndOfValue, // end of stream
|
||||
} |
||||
|
||||
type ChunkedResponder struct { |
||||
recordsQty int |
||||
buf *bytes.Buffer |
||||
dst io.Writer |
||||
} |
||||
|
||||
func NewChunkedResponder(dst io.Writer) *ChunkedResponder { |
||||
s := &ChunkedResponder{ |
||||
recordsQty: 0, |
||||
buf: bytes.NewBuffer(nil), |
||||
dst: dst, |
||||
} |
||||
|
||||
s.buf.Write([]byte{ |
||||
proto.RespPartOfValue, // message type
|
||||
0, 0, 0, 0, // records qty
|
||||
}) |
||||
return s |
||||
} |
||||
|
||||
func (s *ChunkedResponder) BufferRecord(rec []byte) { |
||||
s.buf.Write(rec) |
||||
s.recordsQty++ |
||||
} |
||||
|
||||
func (s *ChunkedResponder) AppendRecord(rec []byte) error { |
||||
s.buf.Write(rec) |
||||
s.recordsQty++ |
||||
|
||||
if s.buf.Len() < 1500 { |
||||
return nil |
||||
} |
||||
|
||||
if err := s.sendBuffered(); err != nil { |
||||
return err |
||||
} |
||||
|
||||
s.buf.Write([]byte{ |
||||
proto.RespPartOfValue, // message type
|
||||
0, 0, 0, 0, // records qty
|
||||
}) |
||||
s.recordsQty = 0 |
||||
return nil |
||||
} |
||||
|
||||
func (s *ChunkedResponder) Flush() error { |
||||
if s.recordsQty > 0 { |
||||
if err := s.sendBuffered(); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
if _, err := s.dst.Write(endMsg); err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (s *ChunkedResponder) sendBuffered() (err error) { |
||||
msg := s.buf.Bytes() |
||||
bin.PutUint32(msg[1:], uint32(s.recordsQty)) |
||||
n, err := s.dst.Write(msg) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if n != len(msg) { |
||||
return fmt.Errorf("incomplete write %d bytes instead of %d", n, len(msg)) |
||||
} |
||||
s.buf.Reset() |
||||
return |
||||
} |
@ -0,0 +1,621 @@ |
||||
package bin |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"math" |
||||
) |
||||
|
||||
const ( |
||||
varInt64SignFlag = 0b01000000 |
||||
maxReadAttempts = 5 |
||||
) |
||||
|
||||
var ( |
||||
ErrNoSpace = errors.New("no space") |
||||
ErrIncompleteWrite = errors.New("incomplete write") |
||||
|
||||
ErrReadOverflow = errors.New("bin: reader returned 'n' > bufsize") |
||||
// ErrNegativeReadCount shows 100% bug in the source reader
|
||||
ErrNegativeReadCount = errors.New("bin: reader returned negative 'n'") |
||||
) |
||||
|
||||
// READ
|
||||
|
||||
func ReadUint16(src io.Reader) (num uint16, err error) { |
||||
var ( |
||||
q = 2 |
||||
arr = make([]byte, q) |
||||
) |
||||
n, err := src.Read(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if n != q { |
||||
return 0, fmt.Errorf("read %d bytes only", n) |
||||
} |
||||
return GetUint16(arr), nil |
||||
} |
||||
|
||||
func ReadUint24AsInt(src io.Reader) (num int, err error) { |
||||
var ( |
||||
q = 3 |
||||
arr = make([]byte, q) |
||||
) |
||||
n, err := src.Read(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if n != q { |
||||
return 0, fmt.Errorf("read %d bytes only", n) |
||||
} |
||||
return GetUint24AsInt(arr), nil |
||||
} |
||||
|
||||
func ReadUint32(src io.Reader) (num uint32, err error) { |
||||
var ( |
||||
q = 4 |
||||
arr = make([]byte, q) |
||||
) |
||||
n, err := src.Read(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if n != q { |
||||
return 0, fmt.Errorf("read %d bytes only", n) |
||||
} |
||||
return GetUint32(arr), nil |
||||
} |
||||
|
||||
func ReadUint64(src io.Reader) (num uint64, err error) { |
||||
var ( |
||||
q = 8 |
||||
arr = make([]byte, q) |
||||
) |
||||
n, err := src.Read(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if n != q { |
||||
return 0, fmt.Errorf("read %d bytes only", n) |
||||
} |
||||
return GetUint64(arr), nil |
||||
} |
||||
|
||||
func ReadFloat64(src io.Reader) (num float64, err error) { |
||||
var ( |
||||
q = 8 |
||||
arr = make([]byte, q) |
||||
) |
||||
n, err := src.Read(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if n != q { |
||||
return 0, fmt.Errorf("read %d bytes only", n) |
||||
} |
||||
return GetFloat64(arr), nil |
||||
} |
||||
|
||||
func ReadUnixtime(src io.Reader) (num int64, err error) { |
||||
var ( |
||||
q = 8 |
||||
arr = make([]byte, q) |
||||
) |
||||
n, err := src.Read(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if n != q { |
||||
return 0, fmt.Errorf("read %d bytes only", n) |
||||
} |
||||
return int64(GetUint64(arr)), nil |
||||
} |
||||
|
||||
// READ VAR
|
||||
|
||||
func ReadVarUint64(src io.Reader) (num uint64, n int, err error) { |
||||
var ( |
||||
p = make([]byte, 1) |
||||
b byte |
||||
) |
||||
for i := range 8 { |
||||
_, err = src.Read(p) |
||||
if err != nil { |
||||
return |
||||
} |
||||
n++ |
||||
b = p[0] |
||||
if b >= 128 { |
||||
num |= uint64(b&127) << uint(i*7) |
||||
return |
||||
} |
||||
num |= uint64(b) << uint(i*7) |
||||
} |
||||
_, err = src.Read(p) |
||||
if err != nil { |
||||
return |
||||
} |
||||
n++ |
||||
num |= uint64(p[0]) << 56 |
||||
return |
||||
} |
||||
|
||||
// GET
|
||||
|
||||
func GetUint16(arr []byte) uint16 { |
||||
return uint16(arr[0]) | (uint16(arr[1]) << 8) |
||||
} |
||||
|
||||
func GetUint16AsInt(arr []byte) int { |
||||
return int(arr[0]) | (int(arr[1]) << 8) |
||||
} |
||||
|
||||
func GetUint24AsInt(arr []byte) int { |
||||
return int(arr[0]) | (int(arr[1]) << 8) | (int(arr[2]) << 16) |
||||
} |
||||
|
||||
func GetUint32(arr []byte) uint32 { |
||||
return uint32(arr[0]) | (uint32(arr[1]) << 8) | |
||||
(uint32(arr[2]) << 16) | (uint32(arr[3]) << 24) |
||||
} |
||||
|
||||
func GetUint32AsInt64(arr []byte) int64 { |
||||
u32 := uint32(arr[0]) | (uint32(arr[1]) << 8) | |
||||
(uint32(arr[2]) << 16) | (uint32(arr[3]) << 24) |
||||
return int64(u32) |
||||
} |
||||
|
||||
func GetUint40(arr []byte) uint64 { |
||||
return uint64(arr[0]) | (uint64(arr[1]) << 8) | |
||||
(uint64(arr[2]) << 16) | (uint64(arr[3]) << 24) | |
||||
(uint64(arr[4]) << 32) |
||||
} |
||||
|
||||
func GetUint48(arr []byte) uint64 { |
||||
return uint64(arr[0]) | (uint64(arr[1]) << 8) | |
||||
(uint64(arr[2]) << 16) | (uint64(arr[3]) << 24) | |
||||
(uint64(arr[4]) << 32) | (uint64(arr[5]) << 40) |
||||
} |
||||
|
||||
func GetUint64(arr []byte) uint64 { |
||||
return uint64(arr[0]) | (uint64(arr[1]) << 8) | |
||||
(uint64(arr[2]) << 16) | (uint64(arr[3]) << 24) | |
||||
(uint64(arr[4]) << 32) | (uint64(arr[5]) << 40) | |
||||
(uint64(arr[6]) << 48) | (uint64(arr[7]) << 56) |
||||
} |
||||
|
||||
func GetFloat32(arr []byte) float32 { |
||||
return math.Float32frombits(GetUint32(arr)) |
||||
} |
||||
|
||||
func GetFloat64(arr []byte) float64 { |
||||
return math.Float64frombits(GetUint64(arr)) |
||||
} |
||||
|
||||
func GetUnixtime(arr []byte) int64 { |
||||
u32 := uint32(arr[0]) | (uint32(arr[1]) << 8) | |
||||
(uint32(arr[2]) << 16) | (uint32(arr[3]) << 24) |
||||
return int64(u32) |
||||
} |
||||
|
||||
func GetVarUint64(arr []byte) (num uint64, n int, err error) { |
||||
var b byte |
||||
for i := range 8 { |
||||
if i >= len(arr) { |
||||
return 0, 0, io.EOF |
||||
} |
||||
b = arr[i] |
||||
if b >= 128 { |
||||
num |= uint64(b&127) << uint(i*7) |
||||
return num, i + 1, nil |
||||
} |
||||
num |= uint64(b) << uint(i*7) |
||||
} |
||||
if len(arr) < 9 { |
||||
return 0, 0, io.EOF |
||||
} |
||||
return num | uint64(arr[8])<<56, 9, nil |
||||
} |
||||
|
||||
func ReverseGetVarUint64(arr []byte) (num uint64, n int, err error) { |
||||
var ( |
||||
b byte |
||||
j = len(arr) - 1 |
||||
) |
||||
for i := range 8 { |
||||
if j < 0 { |
||||
return 0, 0, io.EOF |
||||
} |
||||
b = arr[j] |
||||
if b >= 128 { |
||||
num |= uint64(b&127) << uint(i*7) |
||||
return num, i + 1, nil |
||||
} |
||||
num |= uint64(b) << uint(i*7) |
||||
j-- |
||||
} |
||||
if j < 0 { |
||||
return 0, 0, io.EOF |
||||
} |
||||
return num | uint64(arr[j])<<56, 9, nil |
||||
} |
||||
|
||||
func GetVarInt64(arr []byte) (num int64, n int, err error) { |
||||
u64, n, err := GetVarUint64(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return DecodeZigZag(u64), n, nil |
||||
} |
||||
|
||||
func ReverseGetVarInt64(arr []byte) (num int64, n int, err error) { |
||||
u64, n, err := ReverseGetVarUint64(arr) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return DecodeZigZag(u64), n, nil |
||||
} |
||||
|
||||
// PUT
|
||||
|
||||
func PutUint16(arr []byte, num uint16) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
} |
||||
|
||||
func PutIntAsUint16(arr []byte, num int) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
} |
||||
|
||||
func PutIntAsUint24(arr []byte, num int) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
arr[2] = byte(num >> 16) |
||||
} |
||||
|
||||
func PutUint32(arr []byte, num uint32) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
arr[2] = byte(num >> 16) |
||||
arr[3] = byte(num >> 24) |
||||
} |
||||
|
||||
func PutInt64AsUint32(arr []byte, num int64) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
arr[2] = byte(num >> 16) |
||||
arr[3] = byte(num >> 24) |
||||
} |
||||
|
||||
func PutUint40(arr []byte, num uint64) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
arr[2] = byte(num >> 16) |
||||
arr[3] = byte(num >> 24) |
||||
arr[4] = byte(num >> 32) |
||||
} |
||||
|
||||
func PutUint48(arr []byte, num uint64) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
arr[2] = byte(num >> 16) |
||||
arr[3] = byte(num >> 24) |
||||
arr[4] = byte(num >> 32) |
||||
arr[5] = byte(num >> 40) |
||||
} |
||||
|
||||
func PutUint64(arr []byte, num uint64) { |
||||
arr[0] = byte(num) |
||||
arr[1] = byte(num >> 8) |
||||
arr[2] = byte(num >> 16) |
||||
arr[3] = byte(num >> 24) |
||||
arr[4] = byte(num >> 32) |
||||
arr[5] = byte(num >> 40) |
||||
arr[6] = byte(num >> 48) |
||||
arr[7] = byte(num >> 56) |
||||
} |
||||
|
||||
func PutFloat32(arr []byte, num float32) { |
||||
PutUint32(arr, math.Float32bits(num)) |
||||
} |
||||
|
||||
func PutFloat64(arr []byte, num float64) { |
||||
PutUint64(arr, math.Float64bits(num)) |
||||
} |
||||
|
||||
// WRITE
|
||||
|
||||
func WriteUint16(dst io.Writer, num uint16) error { |
||||
arr := []byte{ |
||||
byte(num), |
||||
byte(num >> 8), |
||||
} |
||||
n, err := dst.Write(arr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if n != 2 { |
||||
return ErrIncompleteWrite |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func WriteUint32(dst io.Writer, num uint32) error { |
||||
arr := []byte{ |
||||
byte(num), |
||||
byte(num >> 8), |
||||
byte(num >> 16), |
||||
byte(num >> 24), |
||||
} |
||||
n, err := dst.Write(arr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if n != 4 { |
||||
return ErrIncompleteWrite |
||||
} |
||||
return err |
||||
} |
||||
|
||||
func WriteFloat64(dst io.Writer, num float64) error { |
||||
arr := make([]byte, 8) |
||||
PutUint64(arr, math.Float64bits(num)) |
||||
n, err := dst.Write(arr) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
if n != 2 { |
||||
return ErrIncompleteWrite |
||||
} |
||||
return err |
||||
} |
||||
|
||||
// WRITE VAR
|
||||
|
||||
func WriteVarUint64(dst io.Writer, num uint64) (int, error) { |
||||
arr := make([]byte, 9) |
||||
for i := range 8 { |
||||
arr[i] = byte(num & 127) |
||||
num >>= 7 |
||||
if num == 0 { |
||||
arr[i] |= 128 |
||||
size := i + 1 |
||||
n, err := dst.Write(arr[:size]) |
||||
if err != nil { |
||||
return n, err |
||||
} |
||||
if n != size { |
||||
return n, ErrIncompleteWrite |
||||
} |
||||
return size, nil |
||||
} |
||||
} |
||||
arr[8] = byte(num) |
||||
n, err := dst.Write(arr) |
||||
if err != nil { |
||||
return n, err |
||||
} |
||||
if n != 9 { |
||||
return n, ErrIncompleteWrite |
||||
} |
||||
return 9, nil |
||||
} |
||||
|
||||
func PutVarUint64(arr []byte, num uint64) (int, error) { |
||||
for i := range 8 { |
||||
if i >= len(arr) { |
||||
return 0, ErrNoSpace |
||||
} |
||||
arr[i] = byte(num & 127) |
||||
num >>= 7 |
||||
if num == 0 { |
||||
arr[i] |= 128 |
||||
return i + 1, nil |
||||
} |
||||
} |
||||
if len(arr) < 9 { |
||||
return 0, ErrNoSpace |
||||
} |
||||
arr[8] = byte(num) |
||||
return 9, nil |
||||
} |
||||
|
||||
func ReversePutVarUint64(arr []byte, num uint64) (int, error) { |
||||
var tmp [9]byte |
||||
for i := range 8 { |
||||
tmp[i] = byte(num & 127) |
||||
num >>= 7 |
||||
if num == 0 { |
||||
tmp[i] |= 128 |
||||
n := i + 1 |
||||
if len(arr) < n { |
||||
return 0, ErrNoSpace |
||||
} |
||||
for j := i; j >= 0; j-- { |
||||
arr[i-j] = tmp[j] |
||||
} |
||||
return n, nil |
||||
} |
||||
} |
||||
tmp[8] = byte(num) |
||||
n := 9 |
||||
if len(arr) < n { |
||||
return 0, ErrNoSpace |
||||
} |
||||
for j := 8; j >= 0; j-- { |
||||
arr[8-j] = tmp[j] |
||||
} |
||||
return n, nil |
||||
} |
||||
|
||||
func PutVarUint64AtEnd(arr []byte, num uint64) (int, error) { |
||||
var ( |
||||
tmp [9]byte |
||||
n int |
||||
) |
||||
for i := range 8 { |
||||
tmp[i] = byte(num & 127) |
||||
num >>= 7 |
||||
if num == 0 { |
||||
tmp[i] |= 128 |
||||
n = i + 1 |
||||
break |
||||
} |
||||
} |
||||
if n == 0 { |
||||
tmp[8] = byte(num) |
||||
n = 9 |
||||
} |
||||
if len(arr) < n { |
||||
return 0, ErrNoSpace |
||||
} |
||||
j := len(arr) - n |
||||
for i := range n { |
||||
arr[j] = tmp[i] |
||||
j++ |
||||
} |
||||
return n, nil |
||||
} |
||||
|
||||
func PutVarInt64(arr []byte, x int64) (int, error) { |
||||
return PutVarUint64(arr, EncodeZigZag(x)) |
||||
} |
||||
|
||||
func PutVarInt64AtEnd(arr []byte, x int64) (int, error) { |
||||
return PutVarUint64AtEnd(arr, EncodeZigZag(x)) |
||||
} |
||||
|
||||
func ReversePutVarInt64(arr []byte, x int64) (int, error) { |
||||
return ReversePutVarUint64(arr, EncodeZigZag(x)) |
||||
} |
||||
|
||||
type KeyComparator interface { |
||||
CompareTo(int) int |
||||
} |
||||
|
||||
func BinarySearch(qty int, keyComparator KeyComparator) (elemIdx int, isFound bool) { |
||||
if qty == 0 { |
||||
return |
||||
} |
||||
a := 0 |
||||
b := qty - 1 |
||||
for { |
||||
var ( |
||||
elemIdx = (b-a)/2 + a |
||||
code = keyComparator.CompareTo(elemIdx) |
||||
) |
||||
|
||||
if code == 1 { |
||||
a = elemIdx + 1 |
||||
if a > b { |
||||
return elemIdx + 1, false |
||||
} |
||||
} else if code == -1 { |
||||
b = elemIdx - 1 |
||||
if b < a { |
||||
return elemIdx, false |
||||
} |
||||
} else { |
||||
return elemIdx, true |
||||
} |
||||
} |
||||
} |
||||
|
||||
func DeleteReverseArrElem(arr []byte, qty int, elemSize int, idx int) { |
||||
dstIdx := len(arr) - idx*elemSize - 1 |
||||
srcIdx := dstIdx - elemSize |
||||
|
||||
end := len(arr) - qty*elemSize |
||||
|
||||
for ; srcIdx >= end; srcIdx-- { |
||||
arr[dstIdx] = arr[srcIdx] |
||||
dstIdx-- |
||||
} |
||||
|
||||
for i := end; i < end+elemSize; i++ { |
||||
arr[i] = 0 |
||||
} |
||||
} |
||||
|
||||
// ZigZag
|
||||
|
||||
// ZigZag encoding: int64 -> uint64
|
||||
func EncodeZigZag(x int64) uint64 { |
||||
return uint64(x<<1) ^ uint64(x>>63) |
||||
} |
||||
|
||||
// ZigZag decoding: uint64 -> int64
|
||||
func DecodeZigZag(u uint64) int64 { |
||||
return int64(u>>1) ^ -(int64(u & 1)) |
||||
} |
||||
|
||||
func ReadN(r io.Reader, n int) (_ []byte, err error) { |
||||
if n < 0 { |
||||
err = fmt.Errorf("wrong n=%d", n) |
||||
return |
||||
} |
||||
buf := make([]byte, n) |
||||
err = ReadNInto(r, buf) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return buf, nil |
||||
} |
||||
|
||||
func ReadNInto(r io.Reader, buf []byte) (err error) { |
||||
if len(buf) == 0 { |
||||
return |
||||
} |
||||
|
||||
var q, total, readAttempts int |
||||
|
||||
for readAttempts < maxReadAttempts { |
||||
bufsize := len(buf) - total |
||||
q, err = r.Read(buf[total:]) |
||||
if q == bufsize { |
||||
return nil |
||||
} |
||||
if err != nil { |
||||
return |
||||
} |
||||
if q > bufsize { |
||||
err = ErrReadOverflow |
||||
return |
||||
} |
||||
if q < 0 { |
||||
err = ErrNegativeReadCount |
||||
return |
||||
} |
||||
if q == 0 { |
||||
readAttempts++ |
||||
} else { |
||||
total += q |
||||
} |
||||
} |
||||
err = io.ErrNoProgress |
||||
return |
||||
} |
||||
|
||||
func CalcVarUint64Length(num uint64) int { |
||||
for i := range 8 { |
||||
num >>= 7 |
||||
if num == 0 { |
||||
return i + 1 |
||||
} |
||||
} |
||||
return 9 |
||||
} |
||||
|
||||
func CalcVarInt64Length(num int64) int { |
||||
u64 := EncodeZigZag(num) |
||||
for i := range 8 { |
||||
u64 >>= 7 |
||||
if u64 == 0 { |
||||
return i + 1 |
||||
} |
||||
} |
||||
return 9 |
||||
} |
@ -0,0 +1,138 @@ |
||||
package bufreader |
||||
|
||||
import ( |
||||
"errors" |
||||
"io" |
||||
) |
||||
|
||||
const ( |
||||
maxReadAttempts = 5 |
||||
defaultBufSize = 1024 |
||||
) |
||||
|
||||
var ( |
||||
// ErrReadOverflow shows 100% bug in the source reader
|
||||
ErrReadOverflow = errors.New("bufreader: reader returned 'n' > bufsize") |
||||
// ErrNegativeReadCount shows 100% bug in the source reader
|
||||
ErrNegativeReadCount = errors.New("bufreader: reader returned negative 'n'") |
||||
) |
||||
|
||||
type BufferedReader struct { |
||||
r io.Reader |
||||
buf []byte |
||||
idx int |
||||
end int |
||||
totalRead int |
||||
} |
||||
|
||||
func New(r io.Reader, bufsize int) *BufferedReader { |
||||
if bufsize == 0 { |
||||
bufsize = defaultBufSize |
||||
} |
||||
return &BufferedReader{ |
||||
r: r, |
||||
buf: make([]byte, bufsize), |
||||
} |
||||
} |
||||
|
||||
func (s *BufferedReader) safeRead(buf []byte) (n int, err error) { |
||||
readAttempts := 0 |
||||
for readAttempts < maxReadAttempts { |
||||
n, err = s.r.Read(buf) |
||||
|
||||
if n > 0 { |
||||
if n > len(buf) { |
||||
return 0, ErrReadOverflow |
||||
} |
||||
if err == io.EOF { |
||||
err = nil |
||||
} |
||||
return |
||||
} |
||||
|
||||
if n < 0 { |
||||
return 0, ErrNegativeReadCount |
||||
} |
||||
|
||||
// n == 0
|
||||
if err != nil { |
||||
return |
||||
} |
||||
readAttempts++ |
||||
} |
||||
return 0, io.ErrNoProgress |
||||
} |
||||
|
||||
func (s *BufferedReader) fill() error { |
||||
n, err := s.safeRead(s.buf) |
||||
s.idx = 0 |
||||
s.end = n |
||||
return err |
||||
} |
||||
|
||||
func (s *BufferedReader) ReadByte() (b byte, err error) { |
||||
if s.idx == s.end { |
||||
if err = s.fill(); err != nil { |
||||
return |
||||
} |
||||
} |
||||
b = s.buf[s.idx] |
||||
s.idx++ |
||||
s.totalRead++ |
||||
return |
||||
} |
||||
|
||||
func (s *BufferedReader) Read(buf []byte) (int, error) { |
||||
size := len(buf) |
||||
|
||||
buffered := s.end - s.idx |
||||
if size <= buffered { |
||||
for i, b := range s.buf[s.idx : s.idx+size] { |
||||
buf[i] = b |
||||
} |
||||
s.idx += size |
||||
s.totalRead += len(buf) |
||||
return size, nil |
||||
} |
||||
for i, b := range s.buf[s.idx:s.end] { |
||||
buf[i] = b |
||||
} |
||||
s.idx = 0 |
||||
s.end = 0 |
||||
|
||||
n := buffered |
||||
rbuf := buf[buffered:] |
||||
|
||||
var ( |
||||
q int |
||||
err error |
||||
) |
||||
for n < size { |
||||
q, err = s.safeRead(rbuf) |
||||
n += q |
||||
rbuf = rbuf[q:] |
||||
|
||||
if err != nil { |
||||
if err == io.EOF && n == size { |
||||
s.totalRead += len(buf) |
||||
return n, nil |
||||
} |
||||
break |
||||
} |
||||
} |
||||
s.totalRead += len(buf[:n]) |
||||
return n, err |
||||
} |
||||
|
||||
func (s *BufferedReader) ReadN(size int) ([]byte, error) { |
||||
buf := make([]byte, size) |
||||
_, err := s.Read(buf) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return buf, nil |
||||
} |
||||
|
||||
func (s *BufferedReader) TotalRead() int { |
||||
return s.totalRead |
||||
} |
@ -0,0 +1,3 @@ |
||||
package chunkenc |
||||
|
||||
const eps = 0.000001 |
@ -0,0 +1,330 @@ |
||||
package chunkenc |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/conbuf" |
||||
) |
||||
|
||||
// REVERSE
|
||||
|
||||
type ReverseCumulativeDeltaCompressor struct { |
||||
buf *conbuf.ContinuousBuffer |
||||
coef float64 |
||||
pos int |
||||
firstValue float64 |
||||
lastDelta uint64 |
||||
length uint16 |
||||
numIdx int |
||||
} |
||||
|
||||
func NewReverseCumulativeDeltaCompressor(buf *conbuf.ContinuousBuffer, size int, fracDigits byte) *ReverseCumulativeDeltaCompressor { |
||||
var coef float64 = 1 |
||||
if fracDigits > 0 { |
||||
coef = math.Pow(10, float64(fracDigits)) |
||||
} |
||||
s := &ReverseCumulativeDeltaCompressor{ |
||||
buf: buf, |
||||
pos: size, |
||||
coef: coef, |
||||
} |
||||
if size > 0 { |
||||
s.restoreState() |
||||
} |
||||
return s |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) restoreState() { |
||||
u64, n, err := s.buf.GetVarUint64(0) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("bug: get first value: %s", err)) |
||||
} |
||||
s.firstValue = float64(u64) / s.coef |
||||
|
||||
if s.pos > n { |
||||
pos := s.pos - 1 |
||||
idxOf8 := uint(8 - s.buf.GetByte(pos)) |
||||
pos-- |
||||
s8 := s.buf.GetByte(pos) |
||||
pos-- |
||||
|
||||
var n int |
||||
s.lastDelta, n, err = s.buf.ReverseGetVarUint64(pos) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("bug: get last delta: %s", err)) |
||||
} |
||||
pos -= n |
||||
s.numIdx = pos + 1 |
||||
|
||||
var flag byte = 1 << idxOf8 |
||||
if (s8 & flag) == flag { |
||||
s.length, _ = s.buf.DecodeRunLength(pos) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) Size() int { |
||||
return s.pos |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) CalcRequiredSpace(value float64) int { |
||||
if s.pos == 0 { |
||||
n := bin.CalcVarUint64Length(uint64(value * s.coef)) |
||||
return n + 3 |
||||
} |
||||
delta := uint64((value-s.firstValue)*s.coef + eps) |
||||
if delta == s.lastDelta { |
||||
if s.length == 0 { |
||||
return 1 |
||||
} else { |
||||
newLength := s.length + 1 |
||||
if newLength < 130 { |
||||
return 0 |
||||
} else if newLength == 130 { |
||||
return 1 |
||||
} else { |
||||
if newLength < 32769 { |
||||
return 0 |
||||
} else { |
||||
n := bin.CalcVarUint64Length(delta) |
||||
n += 2 |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
if s8q == 8 { |
||||
n -= 1 |
||||
} else { |
||||
n -= 2 |
||||
} |
||||
return n |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
n := bin.CalcVarUint64Length(delta) |
||||
n += 2 |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
if s8q == 8 { |
||||
n -= 1 |
||||
} else { |
||||
n -= 2 |
||||
} |
||||
return n |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) Append(value float64) { |
||||
if s.pos == 0 { |
||||
n := s.buf.PutVarUint64(s.pos, uint64(value*s.coef)) |
||||
s.pos += n |
||||
s.firstValue = value |
||||
s.encodeNewDelta(0, 0, 1) |
||||
} else { |
||||
delta := uint64((value-s.firstValue)*s.coef + eps) |
||||
if delta == s.lastDelta { |
||||
if s.length == 0 { |
||||
s.length = 2 |
||||
s.shiftOnePosToRight() |
||||
s.buf.SetByte(s.numIdx-1, 0) |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
s.buf.SetFlag(s.pos-2, 1<<(8-s8q)) |
||||
} else { |
||||
s.length++ |
||||
if s.length < 130 { |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length-2)) |
||||
} else if s.length == 130 { |
||||
s.shiftOnePosToRight() |
||||
s.encode2bLength() |
||||
} else { |
||||
if s.length < 32769 { |
||||
s.encode2bLength() |
||||
} else { |
||||
s.appendNewDelta(delta) |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
s.appendNewDelta(delta) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) appendNewDelta(delta uint64) { |
||||
s.length = 0 |
||||
|
||||
s8 := s.buf.GetByte(s.pos - 2) |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
|
||||
if s8q == 8 { |
||||
s.pos -= 1 |
||||
s8 = 0 |
||||
s8q = 1 |
||||
} else { |
||||
s.pos -= 2 |
||||
s8q++ |
||||
} |
||||
|
||||
s.encodeNewDelta(delta, s8, s8q) |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) encodeNewDelta(delta uint64, s8 byte, s8q byte) { |
||||
s.lastDelta = delta |
||||
s.numIdx = s.pos |
||||
n := s.buf.ReversePutVarUint64(s.pos, s.lastDelta) |
||||
s.pos += n |
||||
s.buf.SetByte(s.pos, s8) |
||||
s.pos++ |
||||
s.buf.SetByte(s.pos, s8q) |
||||
s.pos++ |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) shiftOnePosToRight() { |
||||
s.buf.ShiftOnePosToRight(s.numIdx, s.pos) |
||||
s.pos++ |
||||
s.numIdx++ |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) encode2bLength() { |
||||
num := s.length - 2 |
||||
s.buf.SetByte(s.numIdx-1, byte(num&127)|128) |
||||
s.buf.SetByte(s.numIdx-2, byte(num>>7)) |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaCompressor) DeleteLast() { |
||||
var ( |
||||
s8q = s.buf.GetByte(s.pos - 1) |
||||
s8 = s.buf.GetByte(s.pos - 2) |
||||
flag byte = 1 << uint(8-s8q) |
||||
) |
||||
|
||||
if s.length > 0 { |
||||
if s.length == 2 { |
||||
s.length = 0 |
||||
s.buf.UnsetFlag(s.pos-2, flag) |
||||
s.buf.ShiftOnePosToLeft(s.numIdx, s.pos) |
||||
s.numIdx-- |
||||
s.pos-- |
||||
} else if s.length < 130 { |
||||
s.length-- |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length)-2) |
||||
} else if s.length == 130 { |
||||
s.length-- |
||||
s.buf.ShiftOnePosToLeft(s.numIdx, s.pos) |
||||
s.numIdx-- |
||||
s.pos-- |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length)-2) |
||||
} else { |
||||
s.length-- |
||||
s.encode2bLength() |
||||
} |
||||
} else { |
||||
if s8q > 1 { |
||||
s8q-- |
||||
flag = 1 << uint(8-s8q) |
||||
s.pos = s.numIdx + 2 |
||||
s.buf.SetByte(s.pos-2, s8) |
||||
s.buf.SetByte(s.pos-1, s8q) |
||||
} else { |
||||
s.pos = s.numIdx + 1 |
||||
s.buf.SetByte(s.pos-1, 8) |
||||
s8 = s.buf.GetByte(s.pos - 2) |
||||
flag = 1 |
||||
} |
||||
var ( |
||||
pos = s.pos - 3 |
||||
n int |
||||
err error |
||||
) |
||||
s.lastDelta, n, err = s.buf.ReverseGetVarUint64(pos) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.numIdx = pos - n |
||||
if (s8 & flag) == flag { |
||||
s.length, _ = s.buf.DecodeRunLength(s.numIdx - 1) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type ReverseCumulativeDeltaDecompressor struct { |
||||
buf *conbuf.ContinuousBuffer |
||||
pos int |
||||
bound int |
||||
firstValue float64 |
||||
lastValue float64 |
||||
length uint16 |
||||
coef float64 |
||||
idxOf8 uint |
||||
s8 byte |
||||
step byte |
||||
} |
||||
|
||||
func NewReverseCumulativeDeltaDecompressor(buf *conbuf.ContinuousBuffer, size int, fracDigits byte) *ReverseCumulativeDeltaDecompressor { |
||||
var coef float64 = 1 |
||||
if fracDigits > 0 { |
||||
coef = math.Pow(10, float64(fracDigits)) |
||||
} |
||||
return &ReverseCumulativeDeltaDecompressor{ |
||||
buf: buf, |
||||
coef: coef, |
||||
pos: size, |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaDecompressor) NextValue() (value float64, done bool) { |
||||
if s.step > 0 { |
||||
if s.length > 0 { |
||||
s.length-- |
||||
return s.lastValue, false |
||||
} |
||||
if s.pos < s.bound { |
||||
return 0, true |
||||
} |
||||
if s.idxOf8 == 0 { |
||||
s.s8 = s.buf.GetByte(s.pos) |
||||
s.pos-- |
||||
} |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastValue, false |
||||
} |
||||
u64, n, err := s.buf.GetVarUint64(0) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.firstValue = float64(u64) / s.coef |
||||
s.bound = n |
||||
s.pos-- |
||||
s.idxOf8 = uint(8 - s.buf.GetByte(s.pos)) |
||||
s.pos-- |
||||
s.s8 = s.buf.GetByte(s.pos) |
||||
s.pos-- |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
s.step = 1 |
||||
return s.lastValue, false |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaDecompressor) readVar() { |
||||
u64, n, err := s.buf.ReverseGetVarUint64(s.pos) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.pos -= n |
||||
s.lastValue = s.firstValue + float64(u64)/s.coef |
||||
|
||||
var flag byte = 1 << s.idxOf8 |
||||
if (s.s8 & flag) == flag { |
||||
s.length, n = s.buf.DecodeRunLength(s.pos) |
||||
s.pos -= n |
||||
} |
||||
if s.idxOf8 == 7 { |
||||
s.idxOf8 = 0 |
||||
} else { |
||||
s.idxOf8++ |
||||
} |
||||
} |
@ -0,0 +1,345 @@ |
||||
package chunkenc |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/conbuf" |
||||
) |
||||
|
||||
// REVERSE
|
||||
|
||||
type ReverseInstantDeltaCompressor struct { |
||||
buf *conbuf.ContinuousBuffer |
||||
coef float64 |
||||
pos int |
||||
firstValue float64 |
||||
lastDelta int64 |
||||
length uint16 |
||||
numIdx int |
||||
} |
||||
|
||||
func NewReverseInstantDeltaCompressor(buf *conbuf.ContinuousBuffer, size int, fracDigits byte) *ReverseInstantDeltaCompressor { |
||||
var coef float64 = 1 |
||||
if fracDigits > 0 { |
||||
coef = math.Pow(10, float64(fracDigits)) |
||||
} |
||||
s := &ReverseInstantDeltaCompressor{ |
||||
buf: buf, |
||||
pos: size, |
||||
coef: coef, |
||||
} |
||||
if size > 0 { |
||||
s.restoreState() |
||||
} |
||||
return s |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) restoreState() { |
||||
i64, n, err := s.buf.GetVarInt64(0) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("bug: get first value: %s", err)) |
||||
} |
||||
s.firstValue = float64(i64) / s.coef |
||||
|
||||
if s.pos > n { |
||||
pos := s.pos - 1 |
||||
idxOf8 := uint(8 - s.buf.GetByte(pos)) |
||||
pos-- |
||||
s8 := s.buf.GetByte(pos) |
||||
pos-- |
||||
|
||||
var n int |
||||
s.lastDelta, n, err = s.buf.ReverseGetVarInt64(pos) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("bug: get last delta: %s", err)) |
||||
} |
||||
pos -= n |
||||
s.numIdx = pos + 1 |
||||
var flag byte = 1 << idxOf8 |
||||
if (s8 & flag) == flag { |
||||
s.length, _ = s.buf.DecodeRunLength(pos) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) Size() int { |
||||
return s.pos |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) CalcRequiredSpace(value float64) int { |
||||
if s.pos == 0 { |
||||
n := bin.CalcVarInt64Length(int64(value * s.coef)) |
||||
return n + 3 |
||||
} |
||||
|
||||
tmp := (value - s.firstValue) * s.coef |
||||
if tmp > 0 { |
||||
tmp += eps |
||||
} else { |
||||
tmp -= eps |
||||
} |
||||
delta := int64(tmp) |
||||
|
||||
if delta == s.lastDelta { |
||||
if s.length == 0 { |
||||
return 1 |
||||
} else { |
||||
newLength := s.length + 1 |
||||
if newLength < 130 { |
||||
return 0 |
||||
} else if newLength == 130 { |
||||
return 1 |
||||
} else { |
||||
if newLength < 32769 { |
||||
return 0 |
||||
} else { |
||||
n := bin.CalcVarInt64Length(delta) |
||||
n += 2 |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
if s8q == 8 { |
||||
n -= 1 |
||||
} else { |
||||
n -= 2 |
||||
} |
||||
return n |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
n := bin.CalcVarInt64Length(delta) |
||||
n += 2 |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
if s8q == 8 { |
||||
n -= 1 |
||||
} else { |
||||
n -= 2 |
||||
} |
||||
return n |
||||
} |
||||
} |
||||
|
||||
// В начале буфера кодирую базовое значение.
|
||||
func (s *ReverseInstantDeltaCompressor) Append(value float64) { |
||||
if s.pos == 0 { |
||||
n := s.buf.PutVarInt64(s.pos, int64(value*s.coef)) |
||||
s.pos += n |
||||
s.firstValue = value |
||||
s.encodeNewDelta(0, 0, 1) |
||||
} else { |
||||
tmp := (value - s.firstValue) * s.coef |
||||
if tmp > 0 { |
||||
tmp += eps |
||||
} else { |
||||
tmp -= eps |
||||
} |
||||
delta := int64(tmp) |
||||
|
||||
if delta == s.lastDelta { |
||||
if s.length == 0 { |
||||
s.length = 2 |
||||
s.shiftOnePosToRight() |
||||
s.buf.SetByte(s.numIdx-1, 0) |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
s.buf.SetFlag(s.pos-2, 1<<(8-s8q)) |
||||
} else { |
||||
s.length++ |
||||
if s.length < 130 { |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length-2)) |
||||
} else if s.length == 130 { |
||||
s.shiftOnePosToRight() |
||||
s.encode2bLength() |
||||
} else { |
||||
if s.length < 32769 { |
||||
s.encode2bLength() |
||||
} else { |
||||
s.appendNewDelta(delta) |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
s.appendNewDelta(delta) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) appendNewDelta(delta int64) { |
||||
s.length = 0 |
||||
|
||||
s8 := s.buf.GetByte(s.pos - 2) |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
|
||||
if s8q == 8 { |
||||
s.pos -= 1 |
||||
s8 = 0 |
||||
s8q = 1 |
||||
} else { |
||||
s.pos -= 2 |
||||
s8q++ |
||||
} |
||||
s.encodeNewDelta(delta, s8, s8q) |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) encodeNewDelta(delta int64, s8 byte, s8q byte) { |
||||
s.lastDelta = delta |
||||
s.numIdx = s.pos |
||||
n := s.buf.ReversePutVarInt64(s.pos, s.lastDelta) |
||||
s.pos += n |
||||
s.buf.SetByte(s.pos, s8) |
||||
s.pos++ |
||||
s.buf.SetByte(s.pos, s8q) |
||||
s.pos++ |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) shiftOnePosToRight() { |
||||
s.buf.ShiftOnePosToRight(s.numIdx, s.pos) |
||||
s.pos++ |
||||
s.numIdx++ |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) encode2bLength() { |
||||
num := s.length - 2 |
||||
s.buf.SetByte(s.numIdx-1, byte(num&127)|128) |
||||
s.buf.SetByte(s.numIdx-2, byte(num>>7)) |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaCompressor) DeleteLast() { |
||||
var ( |
||||
s8q = s.buf.GetByte(s.pos - 1) |
||||
s8 = s.buf.GetByte(s.pos - 2) |
||||
flag byte = 1 << uint(8-s8q) |
||||
) |
||||
|
||||
if s.length > 0 { |
||||
if s.length == 2 { |
||||
s.length = 0 |
||||
s.buf.UnsetFlag(s.pos-2, flag) |
||||
s.buf.ShiftOnePosToLeft(s.numIdx, s.pos) |
||||
s.numIdx-- |
||||
s.pos-- |
||||
} else if s.length < 130 { |
||||
s.length-- |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length)-2) |
||||
} else if s.length == 130 { |
||||
s.length-- |
||||
s.buf.ShiftOnePosToLeft(s.numIdx, s.pos) |
||||
s.numIdx-- |
||||
s.pos-- |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length)-2) |
||||
} else { |
||||
s.length-- |
||||
s.encode2bLength() |
||||
} |
||||
} else { |
||||
if s8q > 1 { |
||||
s8q-- |
||||
flag = 1 << uint(8-s8q) |
||||
s.pos = s.numIdx + 2 |
||||
s.buf.SetByte(s.pos-2, s8) |
||||
s.buf.SetByte(s.pos-1, s8q) |
||||
} else { |
||||
s.pos = s.numIdx + 1 |
||||
s.buf.SetByte(s.pos-1, 8) |
||||
s8 = s.buf.GetByte(s.pos - 2) |
||||
flag = 1 |
||||
} |
||||
var ( |
||||
pos = s.pos - 3 |
||||
n int |
||||
err error |
||||
) |
||||
s.lastDelta, n, err = s.buf.ReverseGetVarInt64(pos) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.numIdx = pos - n |
||||
if (s8 & flag) == flag { |
||||
s.length, _ = s.buf.DecodeRunLength(s.numIdx - 1) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type ReverseInstantDeltaDecompressor struct { |
||||
step byte |
||||
buf *conbuf.ContinuousBuffer |
||||
pos int |
||||
bound int |
||||
firstValue float64 |
||||
lastValue float64 |
||||
length uint16 |
||||
coef float64 |
||||
idxOf8 uint |
||||
s8 byte |
||||
} |
||||
|
||||
func NewReverseInstantDeltaDecompressor(buf *conbuf.ContinuousBuffer, size int, fracDigits byte) *ReverseInstantDeltaDecompressor { |
||||
var coef float64 = 1 |
||||
if fracDigits > 0 { |
||||
coef = math.Pow(10, float64(fracDigits)) |
||||
} |
||||
return &ReverseInstantDeltaDecompressor{ |
||||
buf: buf, |
||||
coef: coef, |
||||
pos: size, |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaDecompressor) NextValue() (value float64, done bool) { |
||||
if s.step > 0 { |
||||
if s.length > 0 { |
||||
s.length-- |
||||
return s.lastValue, false |
||||
} |
||||
if s.pos < s.bound { |
||||
return 0, true |
||||
} |
||||
|
||||
if s.idxOf8 == 0 { |
||||
s.s8 = s.buf.GetByte(s.pos) |
||||
s.pos-- |
||||
} |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastValue, false |
||||
} |
||||
i64, n, err := s.buf.GetVarInt64(0) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.firstValue = float64(i64) / s.coef |
||||
s.bound = n |
||||
s.pos-- |
||||
s.idxOf8 = uint(8 - s.buf.GetByte(s.pos)) |
||||
s.pos-- |
||||
s.s8 = s.buf.GetByte(s.pos) |
||||
s.pos-- |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
s.step = 1 |
||||
return s.lastValue, false |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaDecompressor) readVar() { |
||||
i64, n, err := s.buf.ReverseGetVarInt64(s.pos) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.pos -= n |
||||
s.lastValue = s.firstValue + float64(i64)/s.coef |
||||
|
||||
var flag byte = 1 << s.idxOf8 |
||||
if (s.s8 & flag) == flag { |
||||
s.length, n = s.buf.DecodeRunLength(s.pos) |
||||
s.pos -= n |
||||
} |
||||
if s.idxOf8 == 7 { |
||||
s.idxOf8 = 0 |
||||
} else { |
||||
s.idxOf8++ |
||||
} |
||||
} |
@ -0,0 +1,374 @@ |
||||
package chunkenc |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/conbuf" |
||||
) |
||||
|
||||
// REVERSE
|
||||
|
||||
const ( |
||||
lastUnixtimeIdx = 0 |
||||
baseDeltaIdx = 4 |
||||
) |
||||
|
||||
type ReverseTimeDeltaOfDeltaCompressor struct { |
||||
buf *conbuf.ContinuousBuffer |
||||
pos int |
||||
baseDelta uint32 |
||||
lastUnixtime uint32 |
||||
lastDeltaOfDelta int64 |
||||
length uint16 |
||||
numIdx int |
||||
} |
||||
|
||||
func NewReverseTimeDeltaOfDeltaCompressor(buf *conbuf.ContinuousBuffer, size int) *ReverseTimeDeltaOfDeltaCompressor { |
||||
s := &ReverseTimeDeltaOfDeltaCompressor{ |
||||
buf: buf, |
||||
pos: size, |
||||
} |
||||
if size > 0 { |
||||
s.restoreState() |
||||
} |
||||
return s |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) restoreState() { |
||||
s.lastUnixtime = s.buf.GetUint32(lastUnixtimeIdx) |
||||
if s.pos > 4 { |
||||
u64, _, err := s.buf.GetVarUint64(baseDeltaIdx) |
||||
if err != nil { |
||||
panic(fmt.Sprintf("bug: get base delta: %s", err)) |
||||
} |
||||
s.baseDelta = uint32(u64) |
||||
|
||||
pos := s.pos - 1 |
||||
idxOf8 := uint(8 - s.buf.GetByte(pos)) |
||||
pos-- |
||||
s8 := s.buf.GetByte(pos) |
||||
pos-- |
||||
|
||||
var n int |
||||
s.lastDeltaOfDelta, n, err = s.buf.ReverseGetVarInt64(pos) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
pos -= n |
||||
s.numIdx = pos + 1 |
||||
var flag byte = 1 << idxOf8 |
||||
if (s8 & flag) == flag { |
||||
s.length, _ = s.buf.DecodeRunLength(pos) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) Size() int { |
||||
return s.pos |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) CalcRequiredSpace(unixtime uint32) int { |
||||
if s.pos == 0 { |
||||
return 4 |
||||
} |
||||
|
||||
if s.baseDelta == 0 { |
||||
baseDelta := unixtime - s.lastUnixtime |
||||
n := bin.CalcVarUint64Length(uint64(baseDelta)) |
||||
return n + 3 |
||||
} |
||||
|
||||
deltaOfDelta := int64(unixtime-s.lastUnixtime) - int64(s.baseDelta) |
||||
if deltaOfDelta == s.lastDeltaOfDelta { |
||||
if s.length == 0 { |
||||
return 1 |
||||
} else { |
||||
newLength := s.length + 1 |
||||
if newLength < 130 { |
||||
return 0 |
||||
} else if newLength == 130 { |
||||
return 1 |
||||
} else { |
||||
if newLength < 32769 { |
||||
return 0 |
||||
} else { |
||||
n := bin.CalcVarInt64Length(deltaOfDelta) |
||||
n += 2 |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
if s8q == 8 { |
||||
n -= 1 |
||||
} else { |
||||
n -= 2 |
||||
} |
||||
return n |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
n := bin.CalcVarInt64Length(deltaOfDelta) |
||||
n += 2 |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
if s8q == 8 { |
||||
n -= 1 |
||||
} else { |
||||
n -= 2 |
||||
} |
||||
return n |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) Append(unixtime uint32) { |
||||
if s.pos == 0 { |
||||
s.lastUnixtime = unixtime |
||||
s.buf.PutUint32(lastUnixtimeIdx, unixtime) |
||||
s.pos += 4 |
||||
return |
||||
} |
||||
|
||||
if s.baseDelta == 0 { |
||||
s.baseDelta = unixtime - s.lastUnixtime |
||||
s.lastDeltaOfDelta = 0 |
||||
s.lastUnixtime = unixtime |
||||
s.buf.PutUint32(lastUnixtimeIdx, unixtime) |
||||
|
||||
n := s.buf.PutVarUint64(s.pos, uint64(s.baseDelta)) |
||||
s.pos += n |
||||
s.encodeNewDeltaOfDelta(0, 0, 1) |
||||
return |
||||
} |
||||
|
||||
deltaOfDelta := int64(unixtime-s.lastUnixtime) - int64(s.baseDelta) |
||||
s.lastUnixtime = unixtime |
||||
s.buf.PutUint32(lastUnixtimeIdx, unixtime) |
||||
|
||||
if deltaOfDelta == s.lastDeltaOfDelta { |
||||
if s.length == 0 { |
||||
s.length = 2 |
||||
s.shiftOnePosToRight() |
||||
s.buf.SetByte(s.numIdx-1, 0) |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
s.buf.SetFlag(s.pos-2, 1<<(8-s8q)) |
||||
} else { |
||||
s.length++ |
||||
if s.length < 130 { |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length-2)) |
||||
} else if s.length == 130 { |
||||
s.shiftOnePosToRight() |
||||
s.encode2bLength() |
||||
} else { |
||||
if s.length < 32769 { |
||||
s.encode2bLength() |
||||
} else { |
||||
s.appendNewDeltaOfDelta(deltaOfDelta) |
||||
} |
||||
} |
||||
} |
||||
} else { |
||||
s.appendNewDeltaOfDelta(deltaOfDelta) |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) appendNewDeltaOfDelta(deltaOfDelta int64) { |
||||
s.length = 0 |
||||
s8 := s.buf.GetByte(s.pos - 2) |
||||
s8q := s.buf.GetByte(s.pos - 1) |
||||
|
||||
if s8q == 8 { |
||||
s.pos -= 1 |
||||
s8 = 0 |
||||
s8q = 1 |
||||
} else { |
||||
s.pos -= 2 |
||||
s8q++ |
||||
} |
||||
|
||||
s.encodeNewDeltaOfDelta(deltaOfDelta, s8, s8q) |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) encodeNewDeltaOfDelta(deltaOfDelta int64, s8 byte, s8q byte) { |
||||
s.lastDeltaOfDelta = deltaOfDelta |
||||
s.numIdx = s.pos |
||||
n := s.buf.ReversePutVarInt64(s.pos, deltaOfDelta) |
||||
s.pos += n |
||||
s.buf.SetByte(s.pos, s8) |
||||
s.pos++ |
||||
s.buf.SetByte(s.pos, s8q) |
||||
s.pos++ |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) shiftOnePosToRight() { |
||||
s.buf.ShiftOnePosToRight(s.numIdx, s.pos) |
||||
s.pos++ |
||||
s.numIdx++ |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) encode2bLength() { |
||||
num := s.length - 2 |
||||
s.buf.SetByte(s.numIdx-1, byte(num&127)|128) |
||||
s.buf.SetByte(s.numIdx-2, byte(num>>7)) |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaCompressor) DeleteLast() { |
||||
var ( |
||||
s8q = s.buf.GetByte(s.pos - 1) |
||||
s8 = s.buf.GetByte(s.pos - 2) |
||||
flag byte = 1 << uint(8-s8q) |
||||
) |
||||
|
||||
if s.length > 0 { |
||||
if s.length == 2 { |
||||
s.length = 0 |
||||
s.buf.UnsetFlag(s.pos-2, flag) |
||||
s.buf.ShiftOnePosToLeft(s.numIdx, s.pos) |
||||
s.numIdx-- |
||||
s.pos-- |
||||
} else if s.length < 130 { |
||||
s.length-- |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length)-2) |
||||
} else if s.length == 130 { |
||||
s.length-- |
||||
s.buf.ShiftOnePosToLeft(s.numIdx, s.pos) |
||||
s.numIdx-- |
||||
s.pos-- |
||||
s.buf.SetByte(s.numIdx-1, byte(s.length)-2) |
||||
} else { |
||||
s.length-- |
||||
s.encode2bLength() |
||||
} |
||||
} else { |
||||
if s8q > 1 { |
||||
s8q-- |
||||
flag = 1 << uint(8-s8q) |
||||
s.pos = s.numIdx + 2 |
||||
s.buf.SetByte(s.pos-2, s8) |
||||
s.buf.SetByte(s.pos-1, s8q) |
||||
} else { |
||||
s.pos = s.numIdx + 1 |
||||
s.buf.SetByte(s.pos-1, 8) |
||||
s8 = s.buf.GetByte(s.pos - 2) |
||||
flag = 1 |
||||
} |
||||
var ( |
||||
pos = s.pos - 3 |
||||
n int |
||||
err error |
||||
) |
||||
s.lastDeltaOfDelta, n, err = s.buf.ReverseGetVarInt64(pos) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.numIdx = pos - n |
||||
if (s8 & flag) == flag { |
||||
s.length, _ = s.buf.DecodeRunLength(s.numIdx - 1) |
||||
} |
||||
} |
||||
|
||||
delta := int64(s.baseDelta) + s.lastDeltaOfDelta |
||||
s.lastUnixtime = uint32(int64(s.lastUnixtime) - delta) |
||||
s.buf.PutUint32(lastUnixtimeIdx, s.lastUnixtime) |
||||
} |
||||
|
||||
type ReverseTimeDeltaOfDeltaDecompressor struct { |
||||
step byte |
||||
buf *conbuf.ContinuousBuffer |
||||
pos int |
||||
bound int |
||||
lastUnixtime uint32 |
||||
baseDelta uint32 |
||||
lastDeltaOfDelta int64 |
||||
length uint16 |
||||
idxOf8 uint |
||||
s8 byte |
||||
} |
||||
|
||||
func NewReverseTimeDeltaOfDeltaDecompressor(buf *conbuf.ContinuousBuffer, size int) *ReverseTimeDeltaOfDeltaDecompressor { |
||||
return &ReverseTimeDeltaOfDeltaDecompressor{ |
||||
buf: buf, |
||||
pos: size, |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaDecompressor) NextValue() (value uint32, done bool) { |
||||
if s.step == 0 { |
||||
if s.pos == 0 { |
||||
return 0, true |
||||
} |
||||
s.lastUnixtime = s.buf.GetUint32(lastUnixtimeIdx) |
||||
s.step = 1 |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
if s.step == 1 { |
||||
if s.pos == baseDeltaIdx { |
||||
return 0, true |
||||
} |
||||
u64, n, err := s.buf.GetVarUint64(baseDeltaIdx) |
||||
if err != nil { |
||||
panic("EOF") |
||||
} |
||||
s.bound = baseDeltaIdx + n |
||||
s.baseDelta = uint32(u64) |
||||
|
||||
s.pos-- |
||||
s.idxOf8 = uint(8 - s.buf.GetByte(s.pos)) |
||||
s.pos-- |
||||
s.s8 = s.buf.GetByte(s.pos) |
||||
s.pos-- |
||||
|
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
s.step = 2 |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
if s.length > 0 { |
||||
s.length-- |
||||
delta := int64(s.baseDelta) + s.lastDeltaOfDelta |
||||
s.lastUnixtime = uint32(int64(s.lastUnixtime) - delta) |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
if s.pos < s.bound { |
||||
return 0, true |
||||
} |
||||
|
||||
if s.idxOf8 == 0 { |
||||
s.s8 = s.buf.GetByte(s.pos) |
||||
s.pos-- |
||||
} |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaDecompressor) readVar() { |
||||
var ( |
||||
n int |
||||
err error |
||||
) |
||||
|
||||
s.lastDeltaOfDelta, n, err = s.buf.ReverseGetVarInt64(s.pos) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.pos -= n |
||||
|
||||
delta := int64(s.baseDelta) + s.lastDeltaOfDelta |
||||
s.lastUnixtime = uint32(int64(s.lastUnixtime) - delta) |
||||
|
||||
var flag byte = 1 << s.idxOf8 |
||||
if (s.s8 & flag) == flag { |
||||
s.length, n = s.buf.DecodeRunLength(s.pos) |
||||
s.pos -= n |
||||
} |
||||
if s.idxOf8 == 7 { |
||||
s.idxOf8 = 0 |
||||
} else { |
||||
s.idxOf8++ |
||||
} |
||||
} |
@ -0,0 +1,755 @@ |
||||
package client |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net" |
||||
|
||||
"gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/bufreader" |
||||
"gordenko.dev/dima/diploma/proto" |
||||
) |
||||
|
||||
const ( |
||||
metricKeySize = 4 |
||||
) |
||||
|
||||
type Error struct { |
||||
Code uint16 |
||||
Message string |
||||
} |
||||
|
||||
func (s Error) Error() string { |
||||
return fmt.Sprintf("%d: %s", s.Code, s.Message) |
||||
} |
||||
|
||||
type Connection struct { |
||||
conn net.Conn |
||||
src *bufreader.BufferedReader |
||||
} |
||||
|
||||
func Connect(address string) (*Connection, error) { |
||||
conn, err := net.Dial("tcp", address) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &Connection{ |
||||
conn: conn, |
||||
src: bufreader.New(conn, 1500), |
||||
}, nil |
||||
} |
||||
|
||||
func (s *Connection) String() string { |
||||
return s.conn.LocalAddr().String() |
||||
} |
||||
|
||||
func (s *Connection) Close() { |
||||
s.conn.Close() |
||||
} |
||||
|
||||
func (s *Connection) mustSuccess(reader *bufreader.BufferedReader) (err error) { |
||||
code, err := reader.ReadByte() |
||||
if err != nil { |
||||
return fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespSuccess: |
||||
return nil // ok
|
||||
|
||||
case proto.RespError: |
||||
return s.onError() |
||||
|
||||
default: |
||||
return fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
|
||||
type Metric struct { |
||||
MetricID uint32 |
||||
MetricType diploma.MetricType |
||||
FracDigits byte |
||||
} |
||||
|
||||
func (s *Connection) AddMetric(req Metric) error { |
||||
arr := []byte{ |
||||
proto.TypeAddMetric, |
||||
0, 0, 0, 0, //
|
||||
byte(req.MetricType), |
||||
byte(req.FracDigits), |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return err |
||||
} |
||||
return s.mustSuccess(s.src) |
||||
} |
||||
|
||||
func (s *Connection) GetMetric(metricID uint32) (*Metric, error) { |
||||
arr := []byte{ |
||||
proto.TypeGetMetric, |
||||
0, 0, 0, 0, |
||||
} |
||||
bin.PutUint32(arr[1:], metricID) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespValue: |
||||
arr, err := s.src.ReadN(6) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read body: %s", err) |
||||
} |
||||
|
||||
return &Metric{ |
||||
MetricID: bin.GetUint32(arr), |
||||
MetricType: diploma.MetricType(arr[4]), |
||||
FracDigits: arr[5], |
||||
}, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onMaybeError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
|
||||
func (s *Connection) DeleteMetric(metricID uint32) error { |
||||
arr := []byte{ |
||||
proto.TypeDeleteMetric, |
||||
0, 0, 0, 0, //
|
||||
} |
||||
bin.PutUint32(arr[1:], metricID) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return err |
||||
} |
||||
return s.mustSuccess(s.src) |
||||
} |
||||
|
||||
type AppendMeasureReq struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
func (s *Connection) AppendMeasure(req AppendMeasureReq) (err error) { |
||||
arr := []byte{ |
||||
proto.TypeAppendMeasure, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // timestamp
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // value
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Timestamp) |
||||
bin.PutFloat64(arr[9:], req.Value) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return err |
||||
} |
||||
return s.mustSuccess(s.src) |
||||
} |
||||
|
||||
type AppendMeasuresReq struct { |
||||
MetricID uint32 |
||||
Measures []Measure |
||||
} |
||||
|
||||
type Measure struct { |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
func (s *Connection) AppendMeasures(req AppendMeasuresReq) (err error) { |
||||
if len(req.Measures) > 65535 { |
||||
return fmt.Errorf("wrong measures qty: %d", len(req.Measures)) |
||||
} |
||||
var ( |
||||
prefixSize = 7 |
||||
recordSize = 12 |
||||
arr = make([]byte, prefixSize+len(req.Measures)*recordSize) |
||||
) |
||||
arr[0] = proto.TypeAppendMeasures |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint16(arr[5:], uint16(len(req.Measures))) |
||||
pos := prefixSize |
||||
for _, measure := range req.Measures { |
||||
bin.PutUint32(arr[pos:], measure.Timestamp) |
||||
bin.PutFloat64(arr[pos+4:], measure.Value) |
||||
pos += recordSize |
||||
} |
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return err |
||||
} |
||||
return s.mustSuccess(s.src) |
||||
} |
||||
|
||||
type InstantMeasure struct { |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
func (s *Connection) ListAllInstantMeasures(metricID uint32) ([]InstantMeasure, error) { |
||||
arr := []byte{ |
||||
proto.TypeListAllInstantMeasures, |
||||
0, 0, 0, 0, // metricID
|
||||
} |
||||
bin.PutUint32(arr[1:], metricID) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var ( |
||||
result []InstantMeasure |
||||
tmp = make([]byte, 12) |
||||
) |
||||
|
||||
for { |
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespPartOfValue: |
||||
q, err := bin.ReadUint32(s.src) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read records qty: %s", err) |
||||
} |
||||
|
||||
for i := range int(q) { |
||||
err = bin.ReadNInto(s.src, tmp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read record #%d: %s", i, err) |
||||
} |
||||
|
||||
result = append(result, InstantMeasure{ |
||||
Timestamp: bin.GetUint32(tmp), |
||||
Value: bin.GetFloat64(tmp[4:]), |
||||
}) |
||||
} |
||||
|
||||
case proto.RespEndOfValue: |
||||
return result, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Connection) ListInstantMeasures(req proto.ListInstantMeasuresReq) ([]InstantMeasure, error) { |
||||
arr := []byte{ |
||||
proto.TypeListInstantMeasures, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // since
|
||||
0, 0, 0, 0, // until
|
||||
byte(req.FirstHourOfDay), |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Since) |
||||
bin.PutUint32(arr[9:], req.Until) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var ( |
||||
result []InstantMeasure |
||||
tmp = make([]byte, 12) |
||||
) |
||||
|
||||
for { |
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespPartOfValue: |
||||
q, err := bin.ReadUint32(s.src) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read records qty: %s", err) |
||||
} |
||||
|
||||
for i := range int(q) { |
||||
err = bin.ReadNInto(s.src, tmp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read record #%d: %s", i, err) |
||||
} |
||||
|
||||
result = append(result, InstantMeasure{ |
||||
Timestamp: bin.GetUint32(tmp), |
||||
Value: bin.GetFloat64(tmp[4:]), |
||||
}) |
||||
} |
||||
|
||||
case proto.RespEndOfValue: |
||||
return result, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type CumulativeMeasure struct { |
||||
Timestamp uint32 |
||||
Value float64 |
||||
Total float64 |
||||
} |
||||
|
||||
func (s *Connection) ListAllCumulativeMeasures(metricID uint32) ([]CumulativeMeasure, error) { |
||||
arr := []byte{ |
||||
proto.TypeListAllCumulativeMeasures, |
||||
0, 0, 0, 0, // metricID
|
||||
} |
||||
bin.PutUint32(arr[1:], metricID) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var ( |
||||
result []CumulativeMeasure |
||||
tmp = make([]byte, 20) |
||||
) |
||||
|
||||
for { |
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespPartOfValue: |
||||
q, err := bin.ReadUint32(s.src) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read records qty: %s", err) |
||||
} |
||||
|
||||
for i := range int(q) { |
||||
err = bin.ReadNInto(s.src, tmp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read record #%d: %s", i, err) |
||||
} |
||||
|
||||
result = append(result, CumulativeMeasure{ |
||||
Timestamp: bin.GetUint32(tmp), |
||||
Value: bin.GetFloat64(tmp[4:]), |
||||
Total: bin.GetFloat64(tmp[12:]), |
||||
}) |
||||
} |
||||
|
||||
case proto.RespEndOfValue: |
||||
return result, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Connection) ListCumulativeMeasures(req proto.ListCumulativeMeasuresReq) ([]CumulativeMeasure, error) { |
||||
arr := []byte{ |
||||
proto.TypeListCumulativeMeasures, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // since
|
||||
0, 0, 0, 0, // until
|
||||
byte(req.FirstHourOfDay), |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Since) |
||||
bin.PutUint32(arr[9:], req.Until) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var ( |
||||
result []CumulativeMeasure |
||||
tmp = make([]byte, 20) |
||||
) |
||||
|
||||
for { |
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespPartOfValue: |
||||
q, err := bin.ReadUint32(s.src) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read records qty: %s", err) |
||||
} |
||||
|
||||
for i := range int(q) { |
||||
err = bin.ReadNInto(s.src, tmp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read record #%d: %s", i, err) |
||||
} |
||||
|
||||
result = append(result, CumulativeMeasure{ |
||||
Timestamp: bin.GetUint32(tmp), |
||||
Value: bin.GetFloat64(tmp[4:]), |
||||
Total: bin.GetFloat64(tmp[12:]), |
||||
}) |
||||
} |
||||
|
||||
case proto.RespEndOfValue: |
||||
return result, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type InstantPeriod struct { |
||||
Period uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
Min float64 |
||||
Max float64 |
||||
Avg float64 |
||||
} |
||||
|
||||
func (s *Connection) ListInstantPeriods(req proto.ListInstantPeriodsReq) ([]InstantPeriod, error) { |
||||
arr := []byte{ |
||||
proto.TypeListInstantPeriods, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // since
|
||||
0, 0, 0, 0, // until
|
||||
byte(req.GroupBy), |
||||
req.AggregateFuncs, |
||||
byte(req.FirstHourOfDay), |
||||
byte(req.LastDayOfMonth), |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Since) |
||||
bin.PutUint32(arr[9:], req.Until) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var q int |
||||
if (req.AggregateFuncs & diploma.AggregateMin) == diploma.AggregateMin { |
||||
q++ |
||||
} |
||||
if (req.AggregateFuncs & diploma.AggregateMax) == diploma.AggregateMax { |
||||
q++ |
||||
} |
||||
if (req.AggregateFuncs & diploma.AggregateAvg) == diploma.AggregateAvg { |
||||
q++ |
||||
} |
||||
|
||||
var ( |
||||
result []InstantPeriod |
||||
// 12 bytes - period, since, until
|
||||
// q * 8 bytes - min, max, avg
|
||||
tmp = make([]byte, 12+q*8) |
||||
) |
||||
|
||||
for { |
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespPartOfValue: |
||||
q, err := bin.ReadUint32(s.src) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read records qty: %s", err) |
||||
} |
||||
|
||||
for i := range int(q) { |
||||
err = bin.ReadNInto(s.src, tmp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read record #%d: %s", i, err) |
||||
} |
||||
|
||||
var ( |
||||
p = InstantPeriod{ |
||||
Period: bin.GetUint32(tmp[0:]), |
||||
Since: bin.GetUint32(tmp[4:]), |
||||
Until: bin.GetUint32(tmp[8:]), |
||||
} |
||||
// 12 bytes - period, since, until
|
||||
pos = 12 |
||||
) |
||||
|
||||
if (req.AggregateFuncs & diploma.AggregateMin) == diploma.AggregateMin { |
||||
p.Min = bin.GetFloat64(tmp[pos:]) |
||||
pos += 8 |
||||
} |
||||
if (req.AggregateFuncs & diploma.AggregateMax) == diploma.AggregateMax { |
||||
p.Max = bin.GetFloat64(tmp[pos:]) |
||||
pos += 8 |
||||
} |
||||
if (req.AggregateFuncs & diploma.AggregateAvg) == diploma.AggregateAvg { |
||||
p.Avg = bin.GetFloat64(tmp[pos:]) |
||||
} |
||||
result = append(result, p) |
||||
} |
||||
|
||||
case proto.RespEndOfValue: |
||||
return result, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type CumulativePeriod struct { |
||||
Period uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
EndValue float64 |
||||
Total float64 |
||||
} |
||||
|
||||
func (s *Connection) ListCumulativePeriods(req proto.ListCumulativePeriodsReq) ([]CumulativePeriod, error) { |
||||
arr := []byte{ |
||||
proto.TypeListCumulativePeriods, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // since
|
||||
0, 0, 0, 0, // until
|
||||
byte(req.GroupBy), |
||||
byte(req.FirstHourOfDay), |
||||
byte(req.LastDayOfMonth), |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Since) |
||||
bin.PutUint32(arr[9:], req.Until) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var ( |
||||
result []CumulativePeriod |
||||
tmp = make([]byte, 28) |
||||
) |
||||
|
||||
for { |
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespPartOfValue: |
||||
q, err := bin.ReadUint32(s.src) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read records qty: %s", err) |
||||
} |
||||
|
||||
for i := range int(q) { |
||||
err = bin.ReadNInto(s.src, tmp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read record #%d: %s", i, err) |
||||
} |
||||
result = append(result, CumulativePeriod{ |
||||
Period: bin.GetUint32(tmp[0:]), |
||||
Since: bin.GetUint32(tmp[4:]), |
||||
Until: bin.GetUint32(tmp[8:]), |
||||
EndValue: bin.GetFloat64(tmp[12:]), |
||||
Total: bin.GetFloat64(tmp[20:]), |
||||
}) |
||||
} |
||||
|
||||
case proto.RespEndOfValue: |
||||
return result, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
} |
||||
|
||||
type CurrentValue struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
func (s *Connection) ListCurrentValues(metricIDs []uint32) ([]CurrentValue, error) { |
||||
arr := make([]byte, 3+metricKeySize*len(metricIDs)) |
||||
arr[0] = proto.TypeListCurrentValues |
||||
|
||||
bin.PutUint16(arr[1:], uint16(len(metricIDs))) |
||||
|
||||
off := 3 |
||||
for _, metricID := range metricIDs { |
||||
bin.PutUint32(arr[off:], metricID) |
||||
off += metricKeySize |
||||
} |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var ( |
||||
result []CurrentValue |
||||
tmp = make([]byte, 16) |
||||
) |
||||
|
||||
for { |
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespPartOfValue: |
||||
q, err := bin.ReadUint32(s.src) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read records qty: %s", err) |
||||
} |
||||
|
||||
for i := range int(q) { |
||||
err = bin.ReadNInto(s.src, tmp) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read record #%d: %s", i, err) |
||||
} |
||||
|
||||
result = append(result, CurrentValue{ |
||||
MetricID: bin.GetUint32(tmp), |
||||
Timestamp: bin.GetUint32(tmp[4:]), |
||||
Value: bin.GetFloat64(tmp[8:]), |
||||
}) |
||||
} |
||||
|
||||
case proto.RespEndOfValue: |
||||
return result, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Connection) DeleteMeasures(req proto.DeleteMeasuresReq) (err error) { |
||||
arr := []byte{ |
||||
proto.TypeDeleteMeasures, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // since
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Since) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return err |
||||
} |
||||
return s.mustSuccess(s.src) |
||||
} |
||||
|
||||
type RangeTotalResp struct { |
||||
Since uint32 |
||||
SinceValue float64 |
||||
Until uint32 |
||||
UntilValue float64 |
||||
} |
||||
|
||||
func (s *Connection) RangeTotal(req proto.RangeTotalReq) (*RangeTotalResp, error) { |
||||
arr := []byte{ |
||||
proto.TypeGetMetric, |
||||
0, 0, 0, 0, |
||||
0, 0, 0, 0, |
||||
0, 0, 0, 0, |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Since) |
||||
bin.PutUint32(arr[9:], req.MetricID) |
||||
|
||||
if _, err := s.conn.Write(arr); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
code, err := s.src.ReadByte() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read response code: %s", err) |
||||
} |
||||
|
||||
switch code { |
||||
case proto.RespValue: |
||||
arr, err := s.src.ReadN(24) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("read body: %s", err) |
||||
} |
||||
|
||||
return &RangeTotalResp{ |
||||
Since: bin.GetUint32(arr), |
||||
SinceValue: bin.GetFloat64(arr[4:]), |
||||
Until: bin.GetUint32(arr[12:]), |
||||
UntilValue: bin.GetFloat64(arr[16:]), |
||||
}, nil |
||||
|
||||
case proto.RespError: |
||||
return nil, s.onError() |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown reponse code %d", code) |
||||
} |
||||
} |
||||
|
||||
func (s *Connection) onError() error { |
||||
errorCode, err := bin.ReadUint16(s.src) |
||||
if err != nil { |
||||
return fmt.Errorf("read error code: %s", err) |
||||
} |
||||
return Error{ |
||||
Code: errorCode, |
||||
Message: proto.ErrorCodeToText(errorCode), |
||||
} |
||||
} |
||||
|
||||
func (s *Connection) onMaybeError() error { |
||||
errorCode, err := bin.ReadUint16(s.src) |
||||
if err != nil { |
||||
return fmt.Errorf("read error code: %s", err) |
||||
} |
||||
if errorCode == proto.ErrNoMetric { |
||||
return nil |
||||
} |
||||
return Error{ |
||||
Code: errorCode, |
||||
Message: proto.ErrorCodeToText(errorCode), |
||||
} |
||||
} |
@ -0,0 +1,459 @@ |
||||
package conbuf |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
const chunkSize = 512 |
||||
|
||||
var ( |
||||
ErrOutOfRange = errors.New("out of range") |
||||
) |
||||
|
||||
type ContinuousBuffer struct { |
||||
chunks [][]byte |
||||
} |
||||
|
||||
func NewFromBuffer(buf []byte) *ContinuousBuffer { |
||||
var ( |
||||
chunks [][]byte |
||||
copied = 0 |
||||
) |
||||
|
||||
for copied < len(buf) { |
||||
chunk := make([]byte, chunkSize) |
||||
end := min(copied+chunkSize, len(buf)) |
||||
copy(chunk, buf[copied:end]) |
||||
chunks = append(chunks, chunk) |
||||
copied += end - copied |
||||
} |
||||
|
||||
return &ContinuousBuffer{ |
||||
chunks: chunks, |
||||
} |
||||
} |
||||
|
||||
func New(chunks [][]byte) *ContinuousBuffer { |
||||
for i, chunk := range chunks { |
||||
if len(chunk) != chunkSize { |
||||
panic(fmt.Sprintf("wrong chunk #%d size %d", i, len(chunk))) |
||||
} |
||||
} |
||||
return &ContinuousBuffer{ |
||||
chunks: chunks, |
||||
} |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) Chunks() [][]byte { |
||||
return s.chunks |
||||
} |
||||
|
||||
// [0, pos)
|
||||
func (s *ContinuousBuffer) GetByte(idx int) byte { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx >= len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
byteIdx := idx % chunkSize |
||||
return s.chunks[chunkIdx][byteIdx] |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) SetByte(idx int, b byte) { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx > len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
if chunkIdx == len(s.chunks) { |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} |
||||
byteIdx := idx % chunkSize |
||||
s.chunks[chunkIdx][byteIdx] = b |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) SetFlag(idx int, flag byte) { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx > len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
if chunkIdx == len(s.chunks) { |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} |
||||
byteIdx := idx % chunkSize |
||||
s.chunks[chunkIdx][byteIdx] |= flag |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) UnsetFlag(idx int, flag byte) { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx > len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
if chunkIdx == len(s.chunks) { |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} |
||||
byteIdx := idx % chunkSize |
||||
s.chunks[chunkIdx][byteIdx] &^= flag |
||||
} |
||||
|
||||
// [since, until)
|
||||
func (s *ContinuousBuffer) ShiftOnePosToRight(since int, until int) { |
||||
if since < 0 { |
||||
panic("since < 0") |
||||
} |
||||
if since >= until { |
||||
panic("since >= until") |
||||
} |
||||
|
||||
chunkIdx := until / chunkSize |
||||
byteIdx := until % chunkSize |
||||
|
||||
if chunkIdx > len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
|
||||
if chunkIdx == len(s.chunks) { |
||||
if byteIdx == 0 { |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} else { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
} |
||||
|
||||
var ( |
||||
qty = until - since |
||||
prevChunkIdx int |
||||
prevByteIdx int |
||||
) |
||||
|
||||
for range qty { |
||||
prevChunkIdx = chunkIdx |
||||
prevByteIdx = byteIdx - 1 |
||||
if prevByteIdx < 0 { |
||||
prevChunkIdx = chunkIdx - 1 |
||||
prevByteIdx = chunkSize - 1 |
||||
} |
||||
|
||||
s.chunks[chunkIdx][byteIdx] = s.chunks[prevChunkIdx][prevByteIdx] |
||||
|
||||
if byteIdx > 0 { |
||||
byteIdx-- |
||||
} else { |
||||
chunkIdx-- |
||||
byteIdx = chunkSize - 1 |
||||
} |
||||
} |
||||
} |
||||
|
||||
// [since, until)
|
||||
func (s *ContinuousBuffer) ShiftOnePosToLeft(since int, until int) { |
||||
if since <= 0 { |
||||
panic("since <= 0") |
||||
} |
||||
if since >= until { |
||||
panic("since >= until") |
||||
} |
||||
|
||||
chunkIdx := since / chunkSize |
||||
byteIdx := since % chunkSize |
||||
|
||||
if until > len(s.chunks)*chunkSize { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
|
||||
var ( |
||||
qty = until - since |
||||
prevChunkIdx int |
||||
prevByteIdx int |
||||
) |
||||
|
||||
for range qty { |
||||
prevChunkIdx = chunkIdx |
||||
prevByteIdx = byteIdx - 1 |
||||
if prevByteIdx < 0 { |
||||
prevChunkIdx = chunkIdx - 1 |
||||
prevByteIdx = chunkSize - 1 |
||||
} |
||||
|
||||
s.chunks[prevChunkIdx][prevByteIdx] = s.chunks[chunkIdx][byteIdx] |
||||
|
||||
byteIdx++ |
||||
if byteIdx == chunkSize { |
||||
chunkIdx++ |
||||
byteIdx = 0 |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) PutUint32(pos int, num uint32) { |
||||
s.CopyTo(pos, |
||||
[]byte{ |
||||
byte(num), |
||||
byte(num >> 8), |
||||
byte(num >> 16), |
||||
byte(num >> 24), |
||||
}) |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) GetUint32(pos int) uint32 { |
||||
arr := s.Slice(pos, pos+4) |
||||
return uint32(arr[0]) | (uint32(arr[1]) << 8) | |
||||
(uint32(arr[2]) << 16) | (uint32(arr[3]) << 24) |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) ReversePutVarUint64(pos int, num uint64) int { |
||||
var tmp [9]byte |
||||
for i := range 8 { |
||||
tmp[i] = byte(num & 127) |
||||
num >>= 7 |
||||
if num == 0 { |
||||
tmp[i] |= 128 |
||||
n := i + 1 |
||||
s.ReverseCopyTo(pos, tmp[:n]) |
||||
return n |
||||
} |
||||
} |
||||
tmp[8] = byte(num) |
||||
n := 9 |
||||
s.ReverseCopyTo(pos, tmp[:]) |
||||
return n |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) ReverseGetVarUint64(idx int) (num uint64, n int, err error) { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx >= len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
|
||||
var ( |
||||
byteIdx = idx % chunkSize |
||||
chunk = s.chunks[chunkIdx] |
||||
b byte |
||||
) |
||||
|
||||
for i := range 8 { |
||||
b = chunk[byteIdx] |
||||
if b >= 128 { |
||||
num |= uint64(b&127) << uint(i*7) |
||||
return num, i + 1, nil |
||||
} |
||||
num |= uint64(b) << uint(i*7) |
||||
|
||||
if byteIdx > 0 { |
||||
byteIdx-- |
||||
} else { |
||||
if chunkIdx == 0 { |
||||
return 0, 0, io.EOF |
||||
} else { |
||||
chunkIdx-- |
||||
chunk = s.chunks[chunkIdx] |
||||
byteIdx = chunkSize - 1 |
||||
} |
||||
} |
||||
} |
||||
return num | uint64(chunk[byteIdx])<<56, 9, nil |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) ReversePutVarInt64(pos int, x int64) int { |
||||
return s.ReversePutVarUint64(pos, bin.EncodeZigZag(x)) |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) ReverseGetVarInt64(idx int) (int64, int, error) { |
||||
u64, n, err := s.ReverseGetVarUint64(idx) |
||||
if err != nil { |
||||
return 0, 0, err |
||||
} |
||||
return bin.DecodeZigZag(u64), n, nil |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) PutVarUint64(pos int, num uint64) int { |
||||
var tmp [9]byte |
||||
for i := range 8 { |
||||
tmp[i] = byte(num & 127) |
||||
num >>= 7 |
||||
if num == 0 { |
||||
tmp[i] |= 128 |
||||
n := i + 1 |
||||
s.CopyTo(pos, tmp[:n]) |
||||
return n |
||||
} |
||||
} |
||||
tmp[8] = byte(num) |
||||
s.CopyTo(pos, tmp[:]) |
||||
return 9 |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) GetVarUint64(idx int) (num uint64, n int, err error) { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx >= len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
|
||||
var ( |
||||
byteIdx = idx % chunkSize |
||||
chunk = s.chunks[chunkIdx] |
||||
b byte |
||||
) |
||||
|
||||
for i := range 8 { |
||||
b = chunk[byteIdx] |
||||
if b >= 128 { |
||||
num |= uint64(b&127) << uint(i*7) |
||||
return num, i + 1, nil |
||||
} |
||||
num |= uint64(b) << uint(i*7) |
||||
|
||||
byteIdx++ |
||||
if byteIdx == chunkSize { |
||||
chunkIdx++ |
||||
if chunkIdx == len(s.chunks) { |
||||
return 0, 0, io.EOF |
||||
} |
||||
chunk = s.chunks[chunkIdx] |
||||
byteIdx = 0 |
||||
} |
||||
} |
||||
return num | uint64(chunk[byteIdx])<<56, 9, nil |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) PutVarInt64(idx int, x int64) int { |
||||
return s.PutVarUint64(idx, bin.EncodeZigZag(x)) |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) GetVarInt64(idx int) (int64, int, error) { |
||||
u64, n, err := s.GetVarUint64(idx) |
||||
if err != nil { |
||||
return 0, 0, err |
||||
} |
||||
return bin.DecodeZigZag(u64), n, nil |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) CopyTo(idx int, data []byte) { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx > len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
if chunkIdx == len(s.chunks) { |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} |
||||
byteIdx := idx % chunkSize |
||||
chunk := s.chunks[chunkIdx] |
||||
copied := 0 |
||||
|
||||
for _, b := range data { |
||||
chunk[byteIdx] = b |
||||
copied++ |
||||
byteIdx++ |
||||
if byteIdx == chunkSize { |
||||
byteIdx = 0 |
||||
chunkIdx++ |
||||
if chunkIdx == len(s.chunks) { |
||||
if copied == len(data) { |
||||
return |
||||
} |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} |
||||
chunk = s.chunks[chunkIdx] |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) ReverseCopyTo(idx int, data []byte) { |
||||
chunkIdx := idx / chunkSize |
||||
if chunkIdx > len(s.chunks) { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
if chunkIdx == len(s.chunks) { |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} |
||||
byteIdx := idx % chunkSize |
||||
chunk := s.chunks[chunkIdx] |
||||
copied := 0 |
||||
|
||||
for i := len(data) - 1; i >= 0; i-- { |
||||
chunk[byteIdx] = data[i] |
||||
copied++ |
||||
byteIdx++ |
||||
if byteIdx == chunkSize { |
||||
byteIdx = 0 |
||||
chunkIdx++ |
||||
if chunkIdx == len(s.chunks) { |
||||
if copied == len(data) { |
||||
return |
||||
} |
||||
s.chunks = append(s.chunks, make([]byte, chunkSize)) |
||||
} |
||||
chunk = s.chunks[chunkIdx] |
||||
} |
||||
} |
||||
} |
||||
|
||||
// [since, until)
|
||||
func (s *ContinuousBuffer) Slice(since int, until int) []byte { |
||||
if since >= until { |
||||
return nil |
||||
} |
||||
size := len(s.chunks) * chunkSize |
||||
if until >= size { |
||||
panic(ErrOutOfRange) |
||||
} |
||||
|
||||
data := make([]byte, until-since) |
||||
|
||||
chunkIdx := since / chunkSize |
||||
byteIdx := since % chunkSize |
||||
chunk := s.chunks[chunkIdx] |
||||
|
||||
for i := range len(data) { |
||||
data[i] = chunk[byteIdx] |
||||
byteIdx++ |
||||
if byteIdx == chunkSize { |
||||
byteIdx = 0 |
||||
chunkIdx++ |
||||
chunk = s.chunks[chunkIdx] |
||||
} |
||||
} |
||||
return data |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) DecodeRunLength(pos int) (length uint16, n int) { |
||||
b1 := s.GetByte(pos) |
||||
pos-- |
||||
if b1 < 128 { |
||||
length = uint16(b1) |
||||
n = 1 |
||||
} else { |
||||
b2 := s.GetByte(pos) |
||||
length = uint16(b1&127) | (uint16(b2) << 7) |
||||
n = 2 |
||||
} |
||||
length += 2 |
||||
return |
||||
} |
||||
|
||||
func (s *ContinuousBuffer) Copy() *ContinuousBuffer { |
||||
var copies [][]byte |
||||
for _, chunk := range s.chunks { |
||||
buf := make([]byte, chunkSize) |
||||
copy(buf, chunk) |
||||
copies = append(copies, buf) |
||||
} |
||||
return New(copies) |
||||
} |
||||
|
||||
// size to copy
|
||||
func (s *ContinuousBuffer) CopyChunksToOneBuffer(dst []byte, size int) { |
||||
pos := 0 |
||||
for _, chunk := range s.chunks { |
||||
if size >= len(chunk) { |
||||
copy(dst[pos:], chunk) |
||||
size -= len(chunk) |
||||
pos += len(chunk) |
||||
} else { |
||||
copy(dst[pos:], chunk[:size]) |
||||
return |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,4 @@ |
||||
tcpPort = 12345 |
||||
dir = testdir |
||||
redoDir = testdir |
||||
databaseName = test |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,460 @@ |
||||
package database |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"hash/crc32" |
||||
"io" |
||||
"log" |
||||
"net" |
||||
"os" |
||||
"path/filepath" |
||||
"regexp" |
||||
"sync" |
||||
"time" |
||||
|
||||
"gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/atree" |
||||
"gordenko.dev/dima/diploma/atree/redo" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/chunkenc" |
||||
"gordenko.dev/dima/diploma/conbuf" |
||||
"gordenko.dev/dima/diploma/freelist" |
||||
"gordenko.dev/dima/diploma/recovery" |
||||
"gordenko.dev/dima/diploma/txlog" |
||||
) |
||||
|
||||
func JoinSnapshotFileName(dir string, logNumber int) string { |
||||
return filepath.Join(dir, fmt.Sprintf("%d.snapshot", logNumber)) |
||||
} |
||||
|
||||
type metricLockEntry struct { |
||||
XLock bool |
||||
RLocks int |
||||
WaitQueue []any |
||||
} |
||||
|
||||
type Database struct { |
||||
mutex sync.Mutex |
||||
workerSignalCh chan struct{} |
||||
workerQueue []any |
||||
rLocksToRelease []uint32 |
||||
metrics map[uint32]*_metric |
||||
metricLockEntries map[uint32]*metricLockEntry |
||||
dataFreeList *freelist.FreeList |
||||
indexFreeList *freelist.FreeList |
||||
dir string |
||||
databaseName string |
||||
redoDir string |
||||
txlog *txlog.Writer |
||||
atree *atree.Atree |
||||
tcpPort int |
||||
logfile *os.File |
||||
logger *log.Logger |
||||
exitCh chan struct{} |
||||
waitGroup *sync.WaitGroup |
||||
} |
||||
|
||||
type Options struct { |
||||
TCPPort int |
||||
Dir string |
||||
DatabaseName string |
||||
RedoDir string |
||||
Logfile *os.File |
||||
ExitCh chan struct{} |
||||
WaitGroup *sync.WaitGroup |
||||
} |
||||
|
||||
func New(opt Options) (_ *Database, err error) { |
||||
if opt.TCPPort <= 0 { |
||||
return nil, errors.New("TCPPort option is required") |
||||
} |
||||
if opt.Dir == "" { |
||||
return nil, errors.New("Dir option is required") |
||||
} |
||||
if opt.DatabaseName == "" { |
||||
return nil, errors.New("DatabaseName option is required") |
||||
} |
||||
if opt.RedoDir == "" { |
||||
return nil, errors.New("RedoDir option is required") |
||||
} |
||||
if opt.Logfile == nil { |
||||
return nil, errors.New("Logfile option is required") |
||||
} |
||||
if opt.ExitCh == nil { |
||||
return nil, errors.New("ExitCh option is required") |
||||
} |
||||
if opt.WaitGroup == nil { |
||||
return nil, errors.New("WaitGroup option is required") |
||||
} |
||||
|
||||
s := &Database{ |
||||
workerSignalCh: make(chan struct{}, 1), |
||||
dir: opt.Dir, |
||||
databaseName: opt.DatabaseName, |
||||
redoDir: opt.RedoDir, |
||||
metrics: make(map[uint32]*_metric), |
||||
metricLockEntries: make(map[uint32]*metricLockEntry), |
||||
dataFreeList: freelist.New(), |
||||
indexFreeList: freelist.New(), |
||||
tcpPort: opt.TCPPort, |
||||
logfile: opt.Logfile, |
||||
logger: log.New(opt.Logfile, "", log.LstdFlags), |
||||
exitCh: opt.ExitCh, |
||||
waitGroup: opt.WaitGroup, |
||||
} |
||||
return s, nil |
||||
} |
||||
|
||||
func (s *Database) ListenAndServe() (err error) { |
||||
listener, err := net.Listen("tcp", fmt.Sprintf(":%d", s.tcpPort)) |
||||
if err != nil { |
||||
return fmt.Errorf("net.Listen: %s; port=%d", err, s.tcpPort) |
||||
} |
||||
|
||||
s.atree, err = atree.New(atree.Options{ |
||||
Dir: s.dir, |
||||
DatabaseName: s.databaseName, |
||||
RedoDir: s.redoDir, |
||||
DataFreeList: s.dataFreeList, |
||||
IndexFreeList: s.indexFreeList, |
||||
}) |
||||
if err != nil { |
||||
return fmt.Errorf("atree.New: %s", err) |
||||
} |
||||
s.atree.Run() |
||||
|
||||
go s.worker() |
||||
|
||||
s.recovery() |
||||
|
||||
s.logger.Println("database started") |
||||
for { |
||||
// Listen for an incoming connection.
|
||||
conn, err := listener.Accept() |
||||
if err != nil { |
||||
s.logger.Printf("listener.Accept: %s\n", err) |
||||
time.Sleep(time.Second) |
||||
} else { |
||||
go s.handleTCPConn(conn) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Database) recovery() { |
||||
advisor, err := recovery.NewRecoveryAdvisor(recovery.RecoveryAdvisorOptions{ |
||||
Dir: s.dir, |
||||
VerifySnapshot: s.verifySnapshot, |
||||
}) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
recipe, err := advisor.GetRecipe() |
||||
if err != nil { |
||||
diploma.Abort(diploma.GetRecoveryRecipeFailed, err) |
||||
} |
||||
|
||||
var logNumber int |
||||
|
||||
if recipe != nil { |
||||
if recipe.Snapshot != "" { |
||||
err = s.loadSnapshot(recipe.Snapshot) |
||||
if err != nil { |
||||
diploma.Abort(diploma.LoadSnapshotFailed, err) |
||||
} |
||||
} |
||||
for _, changesFileName := range recipe.Changes { |
||||
err = s.replayChanges(changesFileName) |
||||
if err != nil { |
||||
diploma.Abort(diploma.ReplayChangesFailed, err) |
||||
} |
||||
} |
||||
logNumber = recipe.LogNumber |
||||
} |
||||
|
||||
s.txlog, err = txlog.NewWriter(txlog.WriterOptions{ |
||||
Dir: s.dir, |
||||
LogNumber: logNumber, |
||||
AppendToWorkerQueue: s.appendJobToWorkerQueue, |
||||
ExitCh: s.exitCh, |
||||
WaitGroup: s.waitGroup, |
||||
}) |
||||
if err != nil { |
||||
diploma.Abort(diploma.CreateChangesWriterFailed, err) |
||||
|
||||
} |
||||
go s.txlog.Run() |
||||
|
||||
fileNames, err := s.searchREDOFiles() |
||||
if err != nil { |
||||
diploma.Abort(diploma.SearchREDOFilesFailed, err) |
||||
} |
||||
|
||||
if len(fileNames) > 0 { |
||||
for _, fileName := range fileNames { |
||||
err = s.replayREDOFile(fileName) |
||||
if err != nil { |
||||
diploma.Abort(diploma.ReplayREDOFileFailed, err) |
||||
} |
||||
} |
||||
|
||||
for _, fileName := range fileNames { |
||||
err = os.Remove(fileName) |
||||
if err != nil { |
||||
diploma.Abort(diploma.RemoveREDOFileFailed, err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if recipe != nil { |
||||
if recipe.CompleteSnapshot { |
||||
err = s.dumpSnapshot(logNumber) |
||||
if err != nil { |
||||
diploma.Abort(diploma.DumpSnapshotFailed, err) |
||||
} |
||||
} |
||||
|
||||
for _, fileName := range recipe.ToDelete { |
||||
err = os.Remove(fileName) |
||||
if err != nil { |
||||
diploma.Abort(diploma.RemoveRecipeFileFailed, err) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Database) searchREDOFiles() ([]string, error) { |
||||
var ( |
||||
reREDO = regexp.MustCompile(`a\d+\.redo`) |
||||
fileNames []string |
||||
) |
||||
|
||||
entries, err := os.ReadDir(s.redoDir) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
for _, entry := range entries { |
||||
if entry.Type().IsRegular() { |
||||
baseName := entry.Name() |
||||
if reREDO.MatchString(baseName) { |
||||
fileNames = append(fileNames, filepath.Join(s.redoDir, baseName)) |
||||
} |
||||
} |
||||
} |
||||
return fileNames, nil |
||||
} |
||||
|
||||
func (s *Database) replayREDOFile(fileName string) error { |
||||
redoFile, err := redo.ReadREDOFile(redo.ReadREDOFileReq{ |
||||
FileName: fileName, |
||||
DataPageSize: atree.DataPageSize, |
||||
IndexPageSize: atree.IndexPageSize, |
||||
}) |
||||
if err != nil { |
||||
return fmt.Errorf("can't read REDO file %s: %s", fileName, err) |
||||
} |
||||
|
||||
metric, ok := s.metrics[redoFile.MetricID] |
||||
if !ok { |
||||
return fmt.Errorf("has REDOFile, metric %d not found", redoFile.MetricID) |
||||
} |
||||
|
||||
if metric.Until < redoFile.Timestamp { |
||||
waitCh := make(chan struct{}) |
||||
s.atree.ApplyREDO(atree.WriteTask{ |
||||
DataPage: redoFile.DataPage, |
||||
IndexPages: redoFile.IndexPages, |
||||
}) |
||||
<-waitCh |
||||
|
||||
waitCh = s.txlog.WriteAppendedMeasureWithOverflow( |
||||
txlog.AppendedMeasureWithOverflow{ |
||||
MetricID: redoFile.MetricID, |
||||
Timestamp: redoFile.Timestamp, |
||||
Value: redoFile.Value, |
||||
IsDataPageReused: redoFile.IsDataPageReused, |
||||
DataPageNo: redoFile.DataPage.PageNo, |
||||
IsRootChanged: redoFile.IsRootChanged, |
||||
RootPageNo: redoFile.RootPageNo, |
||||
ReusedIndexPages: redoFile.ReusedIndexPages, |
||||
}, |
||||
fileName, |
||||
false, |
||||
) |
||||
<-waitCh |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (s *Database) verifySnapshot(fileName string) (_ bool, err error) { |
||||
file, err := os.Open(fileName) |
||||
if err != nil { |
||||
return |
||||
} |
||||
defer file.Close() |
||||
|
||||
stat, err := file.Stat() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
if stat.Size() <= 4 { |
||||
return false, nil |
||||
} |
||||
|
||||
var ( |
||||
payloadSize = stat.Size() - 4 |
||||
hash = crc32.NewIEEE() |
||||
) |
||||
|
||||
_, err = io.CopyN(hash, file, payloadSize) |
||||
if err != nil { |
||||
return |
||||
} |
||||
calculatedCRC := hash.Sum32() |
||||
|
||||
storedCRC, err := bin.ReadUint32(file) |
||||
if err != nil { |
||||
return |
||||
} |
||||
if storedCRC != calculatedCRC { |
||||
return false, fmt.Errorf("strored CRC %d not equal calculated CRC %d", |
||||
storedCRC, calculatedCRC) |
||||
} |
||||
return true, nil |
||||
} |
||||
|
||||
func (s *Database) replayChanges(fileName string) error { |
||||
walReader, err := txlog.NewReader(txlog.ReaderOptions{ |
||||
FileName: fileName, |
||||
BufferSize: 1024 * 1024, |
||||
}) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
for { |
||||
lsn, records, done, err := walReader.ReadPacket() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
_ = lsn |
||||
|
||||
if done { |
||||
return nil |
||||
} |
||||
|
||||
for _, record := range records { |
||||
if err = s.replayChangesRecord(record); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Database) replayChangesRecord(untyped any) error { |
||||
switch rec := untyped.(type) { |
||||
case txlog.AddedMetric: |
||||
var ( |
||||
values diploma.ValueCompressor |
||||
timestampsBuf = conbuf.New(nil) |
||||
valuesBuf = conbuf.New(nil) |
||||
) |
||||
|
||||
if rec.MetricType == diploma.Cumulative { |
||||
values = chunkenc.NewReverseCumulativeDeltaCompressor( |
||||
valuesBuf, 0, byte(rec.FracDigits)) |
||||
} else { |
||||
values = chunkenc.NewReverseInstantDeltaCompressor( |
||||
valuesBuf, 0, byte(rec.FracDigits)) |
||||
} |
||||
|
||||
s.metrics[rec.MetricID] = &_metric{ |
||||
MetricType: rec.MetricType, |
||||
FracDigits: byte(rec.FracDigits), |
||||
TimestampsBuf: timestampsBuf, |
||||
ValuesBuf: valuesBuf, |
||||
Timestamps: chunkenc.NewReverseTimeDeltaOfDeltaCompressor(timestampsBuf, 0), |
||||
Values: values, |
||||
} |
||||
|
||||
case txlog.DeletedMetric: |
||||
delete(s.metrics, rec.MetricID) |
||||
if len(rec.FreeDataPages) > 0 { |
||||
s.dataFreeList.AddPages(rec.FreeDataPages) |
||||
} |
||||
if len(rec.FreeIndexPages) > 0 { |
||||
s.indexFreeList.AddPages(rec.FreeIndexPages) |
||||
} |
||||
|
||||
case txlog.AppendedMeasure: |
||||
metric, ok := s.metrics[rec.MetricID] |
||||
if ok { |
||||
metric.Timestamps.Append(rec.Timestamp) |
||||
metric.Values.Append(rec.Value) |
||||
|
||||
if metric.Since == 0 { |
||||
metric.Since = rec.Timestamp |
||||
metric.SinceValue = rec.Value |
||||
} |
||||
|
||||
metric.Until = rec.Timestamp |
||||
metric.UntilValue = rec.Value |
||||
} |
||||
|
||||
case txlog.AppendedMeasures: |
||||
metric, ok := s.metrics[rec.MetricID] |
||||
if ok { |
||||
for _, measure := range rec.Measures { |
||||
metric.Timestamps.Append(measure.Timestamp) |
||||
metric.Values.Append(measure.Value) |
||||
|
||||
if metric.Since == 0 { |
||||
metric.Since = measure.Timestamp |
||||
metric.SinceValue = measure.Value |
||||
} |
||||
|
||||
metric.Until = measure.Timestamp |
||||
metric.UntilValue = measure.Value |
||||
} |
||||
} |
||||
|
||||
case txlog.AppendedMeasureWithOverflow: |
||||
metric, ok := s.metrics[rec.MetricID] |
||||
if ok { |
||||
metric.ReinitBy(rec.Timestamp, rec.Value) |
||||
if rec.IsRootChanged { |
||||
metric.RootPageNo = rec.RootPageNo |
||||
} |
||||
metric.LastPageNo = rec.DataPageNo |
||||
// delete free pages
|
||||
if rec.IsDataPageReused { |
||||
s.dataFreeList.DeletePages([]uint32{ |
||||
rec.DataPageNo, |
||||
}) |
||||
} |
||||
if len(rec.ReusedIndexPages) > 0 { |
||||
s.indexFreeList.DeletePages(rec.ReusedIndexPages) |
||||
} |
||||
} |
||||
|
||||
case txlog.DeletedMeasures: |
||||
metric, ok := s.metrics[rec.MetricID] |
||||
if ok { |
||||
metric.DeleteMeasures() |
||||
if len(rec.FreeDataPages) > 0 { |
||||
s.dataFreeList.AddPages(rec.FreeDataPages) |
||||
} |
||||
if len(rec.FreeDataPages) > 0 { |
||||
s.indexFreeList.AddPages(rec.FreeIndexPages) |
||||
} |
||||
} |
||||
|
||||
default: |
||||
diploma.Abort(diploma.UnknownTxLogRecordTypeBug, |
||||
fmt.Errorf("bug: unknown record type %T in TransactionLog", rec)) |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,50 @@ |
||||
package database |
||||
|
||||
import ( |
||||
"errors" |
||||
"io/fs" |
||||
"os" |
||||
"time" |
||||
) |
||||
|
||||
func isFileExist(fileName string) (bool, error) { |
||||
_, err := os.Stat(fileName) |
||||
if err != nil { |
||||
if errors.Is(err, fs.ErrNotExist) { |
||||
return false, nil |
||||
} else { |
||||
return false, err |
||||
} |
||||
} else { |
||||
return true, nil |
||||
} |
||||
} |
||||
|
||||
func (s *Database) appendJobToWorkerQueue(job any) { |
||||
s.mutex.Lock() |
||||
s.workerQueue = append(s.workerQueue, job) |
||||
s.mutex.Unlock() |
||||
|
||||
select { |
||||
case s.workerSignalCh <- struct{}{}: |
||||
default: |
||||
} |
||||
} |
||||
|
||||
func (s *Database) metricRUnlock(metricID uint32) { |
||||
s.mutex.Lock() |
||||
s.rLocksToRelease = append(s.rLocksToRelease, metricID) |
||||
s.mutex.Unlock() |
||||
|
||||
select { |
||||
case s.workerSignalCh <- struct{}{}: |
||||
default: |
||||
} |
||||
} |
||||
|
||||
func correctToFHD(since, until uint32, firstHourOfDay int) (uint32, uint32) { |
||||
duration := time.Duration(firstHourOfDay) * time.Hour |
||||
since = uint32(time.Unix(int64(since), 0).Add(duration).Unix()) |
||||
until = uint32(time.Unix(int64(until), 0).Add(duration).Unix()) |
||||
return since, until |
||||
} |
@ -0,0 +1,71 @@ |
||||
package database |
||||
|
||||
import ( |
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/chunkenc" |
||||
"gordenko.dev/dima/diploma/conbuf" |
||||
) |
||||
|
||||
// METRIC
|
||||
|
||||
type _metric struct { |
||||
MetricType octopus.MetricType |
||||
FracDigits byte |
||||
RootPageNo uint32 |
||||
LastPageNo uint32 |
||||
SinceValue float64 |
||||
Since uint32 |
||||
UntilValue float64 |
||||
Until uint32 |
||||
TimestampsBuf *conbuf.ContinuousBuffer |
||||
ValuesBuf *conbuf.ContinuousBuffer |
||||
Timestamps octopus.TimestampCompressor |
||||
Values octopus.ValueCompressor |
||||
} |
||||
|
||||
func (s *_metric) ReinitBy(timestamp uint32, value float64) { |
||||
s.TimestampsBuf = conbuf.New(nil) |
||||
s.ValuesBuf = conbuf.New(nil) |
||||
//
|
||||
s.Timestamps = chunkenc.NewReverseTimeDeltaOfDeltaCompressor( |
||||
s.TimestampsBuf, 0) |
||||
|
||||
if s.MetricType == octopus.Cumulative { |
||||
s.Values = chunkenc.NewReverseCumulativeDeltaCompressor( |
||||
s.ValuesBuf, 0, s.FracDigits) |
||||
} else { |
||||
s.Values = chunkenc.NewReverseInstantDeltaCompressor( |
||||
s.ValuesBuf, 0, s.FracDigits) |
||||
} |
||||
|
||||
s.Timestamps.Append(timestamp) |
||||
s.Values.Append(value) |
||||
|
||||
s.Since = timestamp |
||||
s.SinceValue = value |
||||
s.Until = timestamp |
||||
s.UntilValue = value |
||||
} |
||||
|
||||
func (s *_metric) DeleteMeasures() { |
||||
s.TimestampsBuf = conbuf.New(nil) |
||||
s.ValuesBuf = conbuf.New(nil) |
||||
//
|
||||
s.Timestamps = chunkenc.NewReverseTimeDeltaOfDeltaCompressor( |
||||
s.TimestampsBuf, 0) |
||||
|
||||
if s.MetricType == octopus.Cumulative { |
||||
s.Values = chunkenc.NewReverseCumulativeDeltaCompressor( |
||||
s.ValuesBuf, 0, s.FracDigits) |
||||
} else { |
||||
s.Values = chunkenc.NewReverseInstantDeltaCompressor( |
||||
s.ValuesBuf, 0, s.FracDigits) |
||||
} |
||||
|
||||
s.RootPageNo = 0 |
||||
s.LastPageNo = 0 |
||||
s.Since = 0 |
||||
s.SinceValue = 0 |
||||
s.Until = 0 |
||||
s.UntilValue = 0 |
||||
} |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,287 @@ |
||||
package database |
||||
|
||||
import ( |
||||
"fmt" |
||||
"hash/crc32" |
||||
"io" |
||||
"os" |
||||
"path/filepath" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/atree" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/chunkenc" |
||||
"gordenko.dev/dima/diploma/conbuf" |
||||
"gordenko.dev/dima/diploma/freelist" |
||||
) |
||||
|
||||
/* |
||||
Формат: |
||||
//lsn - varuint (останній LSN, що змінив дані у RAM)
|
||||
metricsQty - varuint |
||||
[metric]* |
||||
где metric - це: |
||||
metricID - 4b |
||||
metricType - 1b |
||||
fracDigits - 1b |
||||
rootPageNo - 4b |
||||
lastPageNo - 4b |
||||
since - 4b |
||||
sinceValue - 8b |
||||
until - 4b |
||||
untilValue - 8b |
||||
timestamps size - 2b |
||||
values size - 2b |
||||
timestams payload - Nb |
||||
values payload - Nb |
||||
dataFreeList size - varuint |
||||
dataFreeList - Nb |
||||
indexFreeList size - varuint |
||||
indexFreeList - Nb |
||||
CRC32 - 4b |
||||
*/ |
||||
|
||||
const metricHeaderSize = 42 |
||||
|
||||
func (s *Database) dumpSnapshot(logNumber int) (err error) { |
||||
var ( |
||||
fileName = filepath.Join(s.dir, fmt.Sprintf("%d.snapshot", logNumber)) |
||||
hasher = crc32.NewIEEE() |
||||
prefix = make([]byte, metricHeaderSize) |
||||
) |
||||
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, 0770) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
dst := io.MultiWriter(file, hasher) |
||||
|
||||
_, err = bin.WriteVarUint64(dst, uint64(len(s.metrics))) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
for metricID, metric := range s.metrics { |
||||
tSize := metric.Timestamps.Size() |
||||
vSize := metric.Values.Size() |
||||
|
||||
bin.PutUint32(prefix[0:], metricID) |
||||
prefix[4] = byte(metric.MetricType) |
||||
prefix[5] = metric.FracDigits |
||||
bin.PutUint32(prefix[6:], metric.RootPageNo) |
||||
bin.PutUint32(prefix[10:], metric.LastPageNo) |
||||
bin.PutUint32(prefix[14:], metric.Since) |
||||
bin.PutFloat64(prefix[18:], metric.SinceValue) |
||||
bin.PutUint32(prefix[26:], metric.Until) |
||||
bin.PutFloat64(prefix[30:], metric.UntilValue) |
||||
bin.PutUint16(prefix[38:], uint16(tSize)) |
||||
bin.PutUint16(prefix[40:], uint16(vSize)) |
||||
|
||||
_, err = dst.Write(prefix) |
||||
if err != nil { |
||||
return |
||||
} |
||||
// copy timestamps
|
||||
remaining := tSize |
||||
for _, buf := range metric.TimestampsBuf.Chunks() { |
||||
if remaining < len(buf) { |
||||
buf = buf[:remaining] |
||||
} |
||||
_, err = dst.Write(buf) |
||||
if err != nil { |
||||
return |
||||
} |
||||
remaining -= len(buf) |
||||
if remaining == 0 { |
||||
break |
||||
} |
||||
} |
||||
// copy values
|
||||
remaining = vSize |
||||
for _, buf := range metric.ValuesBuf.Chunks() { |
||||
if remaining < len(buf) { |
||||
buf = buf[:remaining] |
||||
} |
||||
_, err = dst.Write(buf) |
||||
if err != nil { |
||||
return |
||||
} |
||||
remaining -= len(buf) |
||||
if remaining == 0 { |
||||
break |
||||
} |
||||
} |
||||
} |
||||
// free data pages
|
||||
err = freeListWriteTo(s.dataFreeList, dst) |
||||
if err != nil { |
||||
return |
||||
} |
||||
// free index pages
|
||||
err = freeListWriteTo(s.indexFreeList, dst) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
bin.WriteUint32(file, hasher.Sum32()) |
||||
|
||||
err = file.Sync() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
err = file.Close() |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
prevLogNumber := logNumber - 1 |
||||
prevChanges := filepath.Join(s.dir, fmt.Sprintf("%d.changes", prevLogNumber)) |
||||
prevSnapshot := filepath.Join(s.dir, fmt.Sprintf("%d.snapshot", prevLogNumber)) |
||||
|
||||
isExist, err := isFileExist(prevChanges) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
if isExist { |
||||
err = os.Remove(prevChanges) |
||||
if err != nil { |
||||
octopus.Abort(octopus.DeletePrevChangesFileFailed, err) |
||||
} |
||||
} |
||||
|
||||
isExist, err = isFileExist(prevSnapshot) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
if isExist { |
||||
err = os.Remove(prevSnapshot) |
||||
if err != nil { |
||||
octopus.Abort(octopus.DeletePrevSnapshotFileFailed, err) |
||||
} |
||||
} |
||||
return |
||||
} |
||||
|
||||
func (s *Database) loadSnapshot(fileName string) (err error) { |
||||
var ( |
||||
hasher = crc32.NewIEEE() |
||||
metricsQty int |
||||
header = make([]byte, metricHeaderSize) |
||||
body = make([]byte, atree.DataPageSize) |
||||
) |
||||
|
||||
file, err := os.Open(fileName) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
src := io.TeeReader(file, hasher) |
||||
u64, _, err := bin.ReadVarUint64(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
metricsQty = int(u64) |
||||
|
||||
for range metricsQty { |
||||
var metric _metric |
||||
err = bin.ReadNInto(src, header) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
metricID := bin.GetUint32(header[0:]) |
||||
metric.MetricType = octopus.MetricType(header[4]) |
||||
metric.FracDigits = header[5] |
||||
metric.RootPageNo = bin.GetUint32(header[6:]) |
||||
metric.LastPageNo = bin.GetUint32(header[10:]) |
||||
metric.Since = bin.GetUint32(header[14:]) |
||||
metric.SinceValue = bin.GetFloat64(header[18:]) |
||||
metric.Until = bin.GetUint32(header[26:]) |
||||
metric.UntilValue = bin.GetFloat64(header[30:]) |
||||
tSize := bin.GetUint16(header[38:]) |
||||
vSize := bin.GetUint16(header[40:]) |
||||
|
||||
buf := body[:tSize] |
||||
err = bin.ReadNInto(src, buf) |
||||
if err != nil { |
||||
return |
||||
} |
||||
metric.TimestampsBuf = conbuf.NewFromBuffer(buf) |
||||
|
||||
buf = body[:vSize] |
||||
err = bin.ReadNInto(src, buf) |
||||
if err != nil { |
||||
return |
||||
} |
||||
metric.ValuesBuf = conbuf.NewFromBuffer(buf) |
||||
|
||||
metric.Timestamps = chunkenc.NewReverseTimeDeltaOfDeltaCompressor( |
||||
metric.TimestampsBuf, int(tSize)) |
||||
|
||||
if metric.MetricType == octopus.Cumulative { |
||||
metric.Values = chunkenc.NewReverseCumulativeDeltaCompressor( |
||||
metric.ValuesBuf, int(vSize), metric.FracDigits) |
||||
} else { |
||||
metric.Values = chunkenc.NewReverseInstantDeltaCompressor( |
||||
metric.ValuesBuf, int(vSize), metric.FracDigits) |
||||
} |
||||
s.metrics[metricID] = &metric |
||||
} |
||||
|
||||
err = restoreFreeList(s.dataFreeList, src) |
||||
if err != nil { |
||||
return fmt.Errorf("restore dataFreeList: %s", err) |
||||
} |
||||
|
||||
err = restoreFreeList(s.indexFreeList, src) |
||||
if err != nil { |
||||
return fmt.Errorf("restore indexFreeList: %s", err) |
||||
} |
||||
|
||||
calculatedChecksum := hasher.Sum32() |
||||
|
||||
writtenChecksum, err := bin.ReadUint32(file) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
if calculatedChecksum != writtenChecksum { |
||||
return fmt.Errorf("calculated checksum %d not equal written checksum %d", calculatedChecksum, writtenChecksum) |
||||
} |
||||
return |
||||
} |
||||
|
||||
// HELPERS
|
||||
|
||||
func freeListWriteTo(freeList *freelist.FreeList, dst io.Writer) error { |
||||
serialized, err := freeList.Serialize() |
||||
if err != nil { |
||||
octopus.Abort(octopus.FailedFreeListSerialize, err) |
||||
} |
||||
_, err = bin.WriteVarUint64(dst, uint64(len(serialized))) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
_, err = dst.Write(serialized) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func restoreFreeList(freeList *freelist.FreeList, src io.Reader) error { |
||||
size, _, err := bin.ReadVarUint64(src) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
serialized, err := bin.ReadN(src, int(size)) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
freeList.Restore(serialized) |
||||
return nil |
||||
} |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,88 @@ |
||||
package diploma |
||||
|
||||
import ( |
||||
"fmt" |
||||
"os" |
||||
) |
||||
|
||||
type MetricType byte |
||||
type GroupBy byte |
||||
|
||||
const ( |
||||
Cumulative MetricType = 1 |
||||
Instant MetricType = 2 |
||||
MaxFracDigits byte = 7 |
||||
|
||||
GroupByHour GroupBy = 1 |
||||
GroupByDay GroupBy = 2 |
||||
GroupByMonth GroupBy = 3 |
||||
|
||||
AggregateMin byte = 1 |
||||
AggregateMax byte = 2 |
||||
AggregateAvg byte = 4 |
||||
) |
||||
|
||||
type TimestampCompressor interface { |
||||
CalcRequiredSpace(uint32) int |
||||
Append(uint32) |
||||
Size() int |
||||
DeleteLast() |
||||
//LastTimestamp() uint32
|
||||
} |
||||
|
||||
type ValueCompressor interface { |
||||
CalcRequiredSpace(float64) int |
||||
Append(float64) |
||||
Size() int |
||||
DeleteLast() |
||||
//LastValue() float64
|
||||
} |
||||
|
||||
type TimestampDecompressor interface { |
||||
NextValue() (uint32, bool) |
||||
} |
||||
|
||||
type ValueDecompressor interface { |
||||
NextValue() (float64, bool) |
||||
} |
||||
|
||||
type AbortCode int |
||||
|
||||
const ( |
||||
// Fatal errors
|
||||
WrongPrevPageNo AbortCode = 1 |
||||
WriteToAtreeFailed AbortCode = 2 |
||||
MaxAtreeSizeExceeded AbortCode = 3 |
||||
FailedWriteToTxLog AbortCode = 4 |
||||
ReferenceCountBug AbortCode = 5 |
||||
WrongResultCodeBug AbortCode = 6 |
||||
RemoveREDOFileFailed AbortCode = 7 |
||||
FailedAtreeRequest AbortCode = 8 |
||||
UnknownTxLogRecordTypeBug AbortCode = 11 |
||||
HasTimestampNoValueBug AbortCode = 12 |
||||
NoMetricBug AbortCode = 13 |
||||
NoLockEntryBug AbortCode = 14 |
||||
NoXLockBug AbortCode = 15 |
||||
MetricAddedBug AbortCode = 16 |
||||
NoRLockBug AbortCode = 17 |
||||
XLockBug AbortCode = 18 |
||||
FailedFreeListSerialize AbortCode = 19 |
||||
UnknownWorkerQueueItemBug AbortCode = 20 |
||||
UnknownMetricWaitQueueItemBug AbortCode = 21 |
||||
//
|
||||
GetRecoveryRecipeFailed AbortCode = 26 |
||||
LoadSnapshotFailed AbortCode = 27 |
||||
ReplayChangesFailed AbortCode = 28 |
||||
CreateChangesWriterFailed AbortCode = 29 |
||||
RemoveRecipeFileFailed AbortCode = 30 |
||||
DumpSnapshotFailed AbortCode = 31 |
||||
SearchREDOFilesFailed AbortCode = 32 |
||||
ReplayREDOFileFailed AbortCode = 33 |
||||
DeletePrevChangesFileFailed AbortCode = 34 |
||||
DeletePrevSnapshotFileFailed AbortCode = 35 |
||||
) |
||||
|
||||
func Abort(code AbortCode, err error) { |
||||
fmt.Println(err) |
||||
os.Exit(int(code)) |
||||
} |
@ -0,0 +1,102 @@ |
||||
package enc |
||||
|
||||
import ( |
||||
"math" |
||||
|
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
type ReverseCumulativeDeltaDecompressor struct { |
||||
buf []byte |
||||
pos int |
||||
bound int |
||||
firstValue float64 |
||||
lastValue float64 |
||||
length uint16 |
||||
coef float64 |
||||
idxOf8 uint |
||||
s8 byte |
||||
} |
||||
|
||||
func NewReverseCumulativeDeltaDecompressor(buf []byte, fracDigits byte) *ReverseCumulativeDeltaDecompressor { |
||||
var coef float64 = 1 |
||||
if fracDigits > 0 { |
||||
coef = math.Pow(10, float64(fracDigits)) |
||||
} |
||||
return &ReverseCumulativeDeltaDecompressor{ |
||||
buf: buf, |
||||
coef: coef, |
||||
pos: len(buf), |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaDecompressor) NextValue() (value float64, done bool) { |
||||
if s.length > 0 { |
||||
s.length-- |
||||
return s.lastValue, false |
||||
} |
||||
if s.pos < s.bound { |
||||
return 0, true |
||||
} |
||||
|
||||
if s.pos == len(s.buf) { |
||||
u64, n, err := bin.GetVarUint64(s.buf) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.firstValue = float64(u64) / s.coef |
||||
s.bound = n |
||||
s.pos-- |
||||
s.idxOf8 = uint(8 - s.buf[s.pos]) |
||||
s.pos-- |
||||
s.s8 = s.buf[s.pos] |
||||
s.pos-- |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastValue, false |
||||
} |
||||
|
||||
if s.idxOf8 == 0 { |
||||
s.s8 = s.buf[s.pos] |
||||
s.pos-- |
||||
} |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastValue, false |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaDecompressor) readVar() { |
||||
u64, n, err := bin.ReverseGetVarUint64(s.buf[:s.pos+1]) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.pos -= n |
||||
s.lastValue = s.firstValue + float64(u64)/s.coef |
||||
|
||||
var flag byte = 1 << s.idxOf8 |
||||
if (s.s8 & flag) == flag { |
||||
s.decodeLength() |
||||
} |
||||
if s.idxOf8 == 7 { |
||||
s.idxOf8 = 0 |
||||
} else { |
||||
s.idxOf8++ |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseCumulativeDeltaDecompressor) decodeLength() { |
||||
b1 := s.buf[s.pos] |
||||
s.pos-- |
||||
if b1 < 128 { |
||||
s.length = uint16(b1) |
||||
} else { |
||||
b2 := s.buf[s.pos] |
||||
s.pos-- |
||||
s.length = uint16(b1&127) | (uint16(b2) << 7) |
||||
} |
||||
s.length += 2 |
||||
} |
@ -0,0 +1,3 @@ |
||||
package enc |
||||
|
||||
const eps = 0.000001 |
@ -0,0 +1,130 @@ |
||||
package enc |
||||
|
||||
import ( |
||||
"fmt" |
||||
"math" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
type ReverseInstantDeltaDecompressor struct { |
||||
buf []byte |
||||
pos int |
||||
bound int |
||||
firstValue float64 |
||||
lastValue float64 |
||||
length uint16 |
||||
coef float64 |
||||
idxOf8 uint |
||||
s8 byte |
||||
} |
||||
|
||||
func NewReverseInstantDeltaDecompressor(buf []byte, fracDigits byte) *ReverseInstantDeltaDecompressor { |
||||
var coef float64 = 1 |
||||
if fracDigits > 0 { |
||||
coef = math.Pow(10, float64(fracDigits)) |
||||
} |
||||
return &ReverseInstantDeltaDecompressor{ |
||||
buf: buf, |
||||
coef: coef, |
||||
pos: len(buf), |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaDecompressor) NextValue() (value float64, done bool) { |
||||
if s.length > 0 { |
||||
s.length-- |
||||
return s.lastValue, false |
||||
} |
||||
if s.pos < s.bound { |
||||
return 0, true |
||||
} |
||||
|
||||
if s.pos == len(s.buf) { |
||||
u64, n, err := bin.GetVarInt64(s.buf) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.firstValue = float64(u64) / s.coef |
||||
s.bound = n |
||||
s.pos-- |
||||
s.idxOf8 = uint(8 - s.buf[s.pos]) |
||||
s.pos-- |
||||
s.s8 = s.buf[s.pos] |
||||
s.pos-- |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastValue, false |
||||
} |
||||
|
||||
if s.idxOf8 == 0 { |
||||
s.s8 = s.buf[s.pos] |
||||
s.pos-- |
||||
} |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastValue, false |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaDecompressor) readVar() { |
||||
i64, n, err := bin.ReverseGetVarInt64(s.buf[:s.pos+1]) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.pos -= n |
||||
s.lastValue = s.firstValue + float64(i64)/s.coef |
||||
|
||||
var flag byte = 1 << s.idxOf8 |
||||
if (s.s8 & flag) == flag { |
||||
s.decodeLength() |
||||
} |
||||
if s.idxOf8 == 7 { |
||||
s.idxOf8 = 0 |
||||
} else { |
||||
s.idxOf8++ |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseInstantDeltaDecompressor) decodeLength() { |
||||
b1 := s.buf[s.pos] |
||||
s.pos-- |
||||
if b1 < 128 { |
||||
s.length = uint16(b1) |
||||
} else { |
||||
b2 := s.buf[s.pos] |
||||
s.pos-- |
||||
s.length = uint16(b1&127) | (uint16(b2) << 7) |
||||
} |
||||
s.length += 2 |
||||
} |
||||
|
||||
func GetValueBounds(valuesBuf []byte, metricType octopus.MetricType, fracDigits byte) (sinceValue, untilValue float64) { |
||||
var decompressor octopus.ValueDecompressor |
||||
switch metricType { |
||||
case octopus.Instant: |
||||
decompressor = NewReverseInstantDeltaDecompressor(valuesBuf, fracDigits) |
||||
case octopus.Cumulative: |
||||
decompressor = NewReverseCumulativeDeltaDecompressor(valuesBuf, fracDigits) |
||||
default: |
||||
panic(fmt.Sprintf("unknown metricType %d", metricType)) |
||||
} |
||||
value, done := decompressor.NextValue() |
||||
if done { |
||||
return |
||||
} |
||||
|
||||
sinceValue = value |
||||
untilValue = value |
||||
for { |
||||
value, done = decompressor.NextValue() |
||||
if done { |
||||
return |
||||
} |
||||
sinceValue = value |
||||
} |
||||
} |
@ -0,0 +1,145 @@ |
||||
package enc |
||||
|
||||
import ( |
||||
"gordenko.dev/dima/diploma/bin" |
||||
) |
||||
|
||||
// REVERSE
|
||||
|
||||
const ( |
||||
lastUnixtimeIdx = 0 |
||||
baseDeltaIdx = 4 |
||||
) |
||||
|
||||
type ReverseTimeDeltaOfDeltaDecompressor struct { |
||||
step byte |
||||
buf []byte |
||||
pos int |
||||
bound int |
||||
lastUnixtime uint32 |
||||
baseDelta uint32 |
||||
lastDeltaOfDelta int64 |
||||
length uint16 |
||||
idxOf8 uint |
||||
s8 byte |
||||
} |
||||
|
||||
func NewReverseTimeDeltaOfDeltaDecompressor(buf []byte) *ReverseTimeDeltaOfDeltaDecompressor { |
||||
return &ReverseTimeDeltaOfDeltaDecompressor{ |
||||
buf: buf, |
||||
pos: len(buf), |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaDecompressor) NextValue() (value uint32, done bool) { |
||||
if s.step == 0 { |
||||
if s.pos == 0 { |
||||
return 0, true |
||||
} |
||||
s.lastUnixtime = bin.GetUint32(s.buf[lastUnixtimeIdx:]) |
||||
s.step = 1 |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
if s.step == 1 { |
||||
if s.pos == baseDeltaIdx { |
||||
return 0, true |
||||
} |
||||
u64, n, err := bin.GetVarUint64(s.buf[baseDeltaIdx:]) |
||||
if err != nil { |
||||
panic("EOF") |
||||
} |
||||
s.bound = baseDeltaIdx + n |
||||
s.baseDelta = uint32(u64) |
||||
|
||||
s.pos-- |
||||
s.idxOf8 = uint(8 - s.buf[s.pos]) |
||||
s.pos-- |
||||
s.s8 = s.buf[s.pos] |
||||
s.pos-- |
||||
|
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
s.step = 2 |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
if s.length > 0 { |
||||
s.length-- |
||||
delta := int64(s.baseDelta) + s.lastDeltaOfDelta |
||||
s.lastUnixtime = uint32(int64(s.lastUnixtime) - delta) |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
if s.pos < s.bound { |
||||
return 0, true |
||||
} |
||||
if s.idxOf8 == 0 { |
||||
s.s8 = s.buf[s.pos] |
||||
s.pos-- |
||||
} |
||||
s.readVar() |
||||
if s.length > 0 { |
||||
s.length-- |
||||
} |
||||
return s.lastUnixtime, false |
||||
} |
||||
|
||||
func GetTimeRange(timestampsBuf []byte) (since, until uint32) { |
||||
decompressor := NewReverseTimeDeltaOfDeltaDecompressor(timestampsBuf) |
||||
value, done := decompressor.NextValue() |
||||
if done { |
||||
return |
||||
} |
||||
|
||||
since = value |
||||
until = value |
||||
for { |
||||
value, done = decompressor.NextValue() |
||||
if done { |
||||
return |
||||
} |
||||
since = value |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaDecompressor) readVar() { |
||||
var ( |
||||
n int |
||||
err error |
||||
) |
||||
|
||||
s.lastDeltaOfDelta, n, err = bin.ReverseGetVarInt64(s.buf[:s.pos+1]) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
s.pos -= n |
||||
|
||||
delta := int64(s.baseDelta) + s.lastDeltaOfDelta |
||||
s.lastUnixtime = uint32(int64(s.lastUnixtime) - delta) |
||||
|
||||
var flag byte = 1 << s.idxOf8 |
||||
if (s.s8 & flag) == flag { |
||||
s.decodeLength() |
||||
} |
||||
if s.idxOf8 == 7 { |
||||
s.idxOf8 = 0 |
||||
} else { |
||||
s.idxOf8++ |
||||
} |
||||
} |
||||
|
||||
func (s *ReverseTimeDeltaOfDeltaDecompressor) decodeLength() { |
||||
b1 := s.buf[s.pos] |
||||
s.pos-- |
||||
if b1 < 128 { |
||||
s.length = uint16(b1) |
||||
} else { |
||||
b2 := s.buf[s.pos] |
||||
s.pos-- |
||||
s.length = uint16(b1&127) | (uint16(b2) << 7) |
||||
} |
||||
s.length += 2 |
||||
} |
@ -0,0 +1,135 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
"os/signal" |
||||
"sync" |
||||
"syscall" |
||||
|
||||
"gopkg.in/ini.v1" |
||||
"gordenko.dev/dima/diploma/database" |
||||
) |
||||
|
||||
func main() { |
||||
var ( |
||||
logfile = os.Stdout |
||||
iniFileName string |
||||
) |
||||
|
||||
flag.Usage = func() { |
||||
fmt.Fprint(flag.CommandLine.Output(), helpMessage) |
||||
fmt.Fprint(flag.CommandLine.Output(), configExample) |
||||
fmt.Fprintf(flag.CommandLine.Output(), mainUsage, os.Args[0]) |
||||
flag.PrintDefaults() |
||||
} |
||||
flag.StringVar(&iniFileName, "c", "database.ini", "path to *.ini config file") |
||||
flag.Parse() |
||||
|
||||
config, err := loadConfig(iniFileName) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
var ( |
||||
exitCh = make(chan struct{}) |
||||
wg = new(sync.WaitGroup) |
||||
) |
||||
|
||||
db, err := database.New(database.Options{ |
||||
TCPPort: config.TcpPort, |
||||
Dir: config.Dir, |
||||
DatabaseName: config.DatabaseName, |
||||
RedoDir: config.REDODir, |
||||
Logfile: logfile, |
||||
ExitCh: exitCh, |
||||
WaitGroup: wg, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("database.New: %s\n", err) |
||||
} |
||||
|
||||
go func() { |
||||
err = db.ListenAndServe() |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
}() |
||||
|
||||
wg.Add(1) |
||||
|
||||
fmt.Fprintf(logfile, "database %q started on port %d.\n", |
||||
config.DatabaseName, config.TcpPort) |
||||
fmt.Fprintln(logfile, config) |
||||
|
||||
sigs := make(chan os.Signal, 1) |
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) |
||||
|
||||
<-sigs |
||||
|
||||
close(exitCh) |
||||
wg.Wait() |
||||
|
||||
fmt.Fprintln(logfile, "database stopped.") |
||||
} |
||||
|
||||
// config file
|
||||
|
||||
const mainUsage = `Usage: |
||||
%s -c path/to/config.ini |
||||
|
||||
` |
||||
|
||||
const helpMessage = `Diploma project. Database. Version: 1.0 |
||||
created by Dmytro Gordenko, 1.e4.kc6@gmail.com |
||||
` |
||||
|
||||
const configExample = ` |
||||
database.ini example: |
||||
|
||||
tcpPort = 12345 |
||||
dir = ../../datadir |
||||
redoDir = ../../datadir |
||||
databaseName = test |
||||
|
||||
` |
||||
|
||||
type Config struct { |
||||
TcpPort int |
||||
Dir string |
||||
REDODir string |
||||
DatabaseName string |
||||
} |
||||
|
||||
func (s Config) String() string { |
||||
return fmt.Sprintf(`starting options: |
||||
tcpPort = %d |
||||
dir = %s |
||||
redoDir = %s |
||||
databaseName = %s |
||||
`, |
||||
s.TcpPort, s.Dir, s.REDODir, s.DatabaseName) |
||||
} |
||||
|
||||
func loadConfig(iniFileName string) (_ Config, err error) { |
||||
file, err := ini.Load(iniFileName) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
conf := Config{} |
||||
|
||||
top := file.Section("") |
||||
|
||||
conf.TcpPort, err = top.Key("tcpPort").Int() |
||||
if err != nil { |
||||
err = fmt.Errorf("'tcpPort' option is required in config file") |
||||
return |
||||
} |
||||
conf.Dir = top.Key("dir").String() |
||||
conf.REDODir = top.Key("redoDir").String() |
||||
conf.DatabaseName = top.Key("databaseName").String() |
||||
return conf, nil |
||||
} |
@ -0,0 +1,377 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"math/rand" |
||||
"os" |
||||
"time" |
||||
|
||||
"gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/client" |
||||
"gordenko.dev/dima/diploma/proto" |
||||
) |
||||
|
||||
// METRICS INFO
|
||||
|
||||
type MetricInfo struct { |
||||
MetricID uint32 `json:"metricID"` |
||||
MetricType diploma.MetricType `json:"metricType"` |
||||
FracDigits int `json:"fracDigits"` |
||||
Since int64 `json:"since"` |
||||
Until int64 `json:"until"` |
||||
Qty int `json:"qty"` |
||||
} |
||||
|
||||
func readMetricInfo(fileName string) (list []MetricInfo, err error) { |
||||
buf, err := os.ReadFile(fileName) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
err = json.Unmarshal(buf, &list) |
||||
return |
||||
} |
||||
|
||||
// RANDOM QUERY GENERATOR
|
||||
|
||||
type QueryRecipe struct { |
||||
MetricID uint32 |
||||
MetricIDs []uint32 |
||||
Method int |
||||
RangeCode int |
||||
Since uint32 |
||||
Until uint32 |
||||
GroupBy diploma.GroupBy |
||||
} |
||||
|
||||
type RandomQueryGenerator struct { |
||||
metrics []MetricInfo |
||||
groupByOptions []diploma.GroupBy |
||||
listCurrentValuesProbability int |
||||
listPeriodsProbability int |
||||
timeRangeProbDistribution []int |
||||
} |
||||
|
||||
type RandomQueryGeneratorOptions struct { |
||||
Metrics []MetricInfo |
||||
ListCurrentValuesProbability int |
||||
ListPeriodsProbability int |
||||
TimeRangeProbabilities []int |
||||
} |
||||
|
||||
func NewRandomQueryGenerator(opt RandomQueryGeneratorOptions) *RandomQueryGenerator { |
||||
if opt.ListCurrentValuesProbability >= 100 { |
||||
panic(fmt.Sprintf("wrong ListCurrentValuesProbability: %d", opt.ListCurrentValuesProbability)) |
||||
} |
||||
|
||||
if opt.ListPeriodsProbability >= 100 { |
||||
panic(fmt.Sprintf("wrong ListPeriodsProbability: %d", opt.ListPeriodsProbability)) |
||||
} |
||||
// check total time range propability
|
||||
var totalTimeRangeProbability int |
||||
for _, p := range opt.TimeRangeProbabilities { |
||||
totalTimeRangeProbability += p |
||||
} |
||||
if totalTimeRangeProbability != 100 { |
||||
panic(fmt.Sprintf("total time range probabilities != 100: %d", totalTimeRangeProbability)) |
||||
} |
||||
|
||||
// create time range probability distribution
|
||||
timeRangeProbDistribution := make([]int, len(opt.TimeRangeProbabilities)) |
||||
timeRangeProbDistribution[0] = opt.TimeRangeProbabilities[0] |
||||
for i := 1; i < len(opt.TimeRangeProbabilities); i++ { |
||||
timeRangeProbDistribution[i] = timeRangeProbDistribution[i-1] + opt.TimeRangeProbabilities[i] |
||||
} |
||||
|
||||
return &RandomQueryGenerator{ |
||||
metrics: opt.Metrics, |
||||
groupByOptions: []diploma.GroupBy{ |
||||
diploma.GroupByHour, |
||||
diploma.GroupByDay, |
||||
diploma.GroupByMonth, |
||||
}, |
||||
listCurrentValuesProbability: opt.ListCurrentValuesProbability, |
||||
listPeriodsProbability: opt.ListPeriodsProbability, |
||||
timeRangeProbDistribution: timeRangeProbDistribution, |
||||
} |
||||
} |
||||
|
||||
func (s *RandomQueryGenerator) GetQueryRecipe() QueryRecipe { |
||||
metric := s.getRandomMetric() |
||||
|
||||
num := rand.Intn(100) |
||||
if num < s.listCurrentValuesProbability { |
||||
qty := 5 + rand.Intn(100) // від 5 до 105
|
||||
return QueryRecipe{ |
||||
MetricIDs: s.listRandomUniqueMetricIDs(qty), |
||||
Method: listCurrentValues, |
||||
} |
||||
} else { |
||||
if metric.MetricType == diploma.Cumulative { |
||||
num = rand.Intn(100) |
||||
if num < s.listPeriodsProbability { |
||||
groupBy := s.groupByOptions[rand.Intn(len(s.groupByOptions))] |
||||
|
||||
var ( |
||||
minDays = 1 |
||||
maxDays = 7 |
||||
) |
||||
|
||||
if groupBy == diploma.GroupByDay { |
||||
minDays = 1 |
||||
maxDays = 30 |
||||
} else if groupBy == diploma.GroupByMonth { |
||||
minDays = 1 |
||||
maxDays = 30 |
||||
} |
||||
|
||||
rangeCode, since, until := s.getRandomTimeRange( |
||||
metric.Since, metric.Until, minDays, maxDays) |
||||
|
||||
return QueryRecipe{ |
||||
MetricID: metric.MetricID, |
||||
Method: listCumulativePeriods, |
||||
RangeCode: rangeCode, |
||||
Since: uint32(since), |
||||
Until: uint32(until), |
||||
GroupBy: groupBy, |
||||
} |
||||
} else { |
||||
var ( |
||||
minDays = 1 |
||||
maxDays = 3 |
||||
) |
||||
|
||||
rangeCode, since, until := s.getRandomTimeRange( |
||||
metric.Since, metric.Until, minDays, maxDays) |
||||
|
||||
return QueryRecipe{ |
||||
MetricID: metric.MetricID, |
||||
Method: listCumulativeMeasures, |
||||
RangeCode: rangeCode, |
||||
Since: uint32(since), |
||||
Until: uint32(until), |
||||
} |
||||
} |
||||
} else { |
||||
num = rand.Intn(100) |
||||
if num < s.listPeriodsProbability { |
||||
groupBy := s.groupByOptions[rand.Intn(len(s.groupByOptions))] |
||||
|
||||
var ( |
||||
minDays = 1 |
||||
maxDays = 7 |
||||
) |
||||
|
||||
if groupBy == diploma.GroupByDay { |
||||
minDays = 1 |
||||
maxDays = 30 |
||||
} else if groupBy == diploma.GroupByMonth { |
||||
minDays = 1 |
||||
maxDays = 30 |
||||
} |
||||
|
||||
rangeCode, since, until := s.getRandomTimeRange( |
||||
metric.Since, metric.Until, minDays, maxDays) |
||||
|
||||
return QueryRecipe{ |
||||
MetricID: metric.MetricID, |
||||
Method: listInstantPeriods, |
||||
RangeCode: rangeCode, |
||||
Since: uint32(since), |
||||
Until: uint32(until), |
||||
GroupBy: groupBy, |
||||
} |
||||
} else { |
||||
var ( |
||||
minDays = 1 |
||||
maxDays = 3 |
||||
) |
||||
|
||||
rangeCode, since, until := s.getRandomTimeRange( |
||||
metric.Since, metric.Until, minDays, maxDays) |
||||
|
||||
return QueryRecipe{ |
||||
MetricID: metric.MetricID, |
||||
Method: listInstantMeasures, |
||||
RangeCode: rangeCode, |
||||
Since: uint32(since), |
||||
Until: uint32(until), |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
// Генерує випадковий набір унікальних metricID з [1, 100]
|
||||
func (s *RandomQueryGenerator) listRandomUniqueMetricIDs(count int) []uint32 { |
||||
// переставляю індекси у випадковому порядку
|
||||
indexes := rand.Perm(len(s.metrics)) |
||||
// копіюю metricID із перших випадкових індексів
|
||||
metricIDs := make([]uint32, count) |
||||
for i := range count { |
||||
metricIDs[i] = s.metrics[indexes[i]].MetricID |
||||
} |
||||
return metricIDs |
||||
} |
||||
|
||||
const ( |
||||
secondsPerDay = 86400 |
||||
dayRange = 0 |
||||
weekRange = 1 |
||||
monthRange = 2 |
||||
randomTimeRange = 3 |
||||
) |
||||
|
||||
// Випадковий часовий діапазон
|
||||
func (s *RandomQueryGenerator) getRandomTimeRange(start, end int64, minDays, maxDays int) (int, int64, int64) { |
||||
var ( |
||||
since int64 |
||||
until int64 |
||||
num = rand.Intn(100) |
||||
rangeCode int |
||||
threshold int |
||||
) |
||||
for rangeCode, threshold = range s.timeRangeProbDistribution { |
||||
if num < threshold { |
||||
break |
||||
} |
||||
} |
||||
|
||||
switch rangeCode { |
||||
case dayRange: |
||||
since = end - secondsPerDay |
||||
until = end |
||||
|
||||
case weekRange: |
||||
since = end - 7*secondsPerDay |
||||
until = end |
||||
|
||||
case monthRange: |
||||
since = end - 30*secondsPerDay |
||||
until = end |
||||
|
||||
case randomTimeRange: |
||||
if start == end { |
||||
return rangeCode, start, end |
||||
} |
||||
// Випадковий момент часу для since
|
||||
since = start + rand.Int63n(end-start) |
||||
// Випадкова тривалість у днях (але не виходити за межу end)
|
||||
durationInDays := minDays + rand.Intn(maxDays-minDays) |
||||
|
||||
until = since + int64(durationInDays)*secondsPerDay |
||||
if until > end { |
||||
until = end |
||||
} |
||||
} |
||||
return rangeCode, since, until |
||||
} |
||||
|
||||
func (s *RandomQueryGenerator) getRandomMetric() MetricInfo { |
||||
return s.metrics[rand.Intn(len(s.metrics))] |
||||
} |
||||
|
||||
// EXECUTE QUERY
|
||||
|
||||
func execQuery(conn *client.Connection, queryGenerator *RandomQueryGenerator, stat *WorkerStat) (err error) { |
||||
recipe := queryGenerator.GetQueryRecipe() |
||||
|
||||
var elapsedTime time.Duration |
||||
|
||||
switch recipe.Method { |
||||
case listCurrentValues: |
||||
t1 := time.Now() |
||||
_, err := conn.ListCurrentValues(recipe.MetricIDs) |
||||
elapsedTime = time.Since(t1) |
||||
stat.ElapsedTime += elapsedTime |
||||
stat.Queries++ |
||||
stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime |
||||
stat.MethodCalls[recipe.Method]++ |
||||
if err != nil { |
||||
return fmt.Errorf("ListCurrentValues: %s", err) |
||||
} |
||||
|
||||
case listInstantMeasures: |
||||
t1 := time.Now() |
||||
_, err := conn.ListInstantMeasures(proto.ListInstantMeasuresReq{ |
||||
MetricID: recipe.MetricID, |
||||
Since: recipe.Since, |
||||
Until: recipe.Until, |
||||
}) |
||||
elapsedTime = time.Since(t1) |
||||
stat.ElapsedTime += elapsedTime |
||||
stat.Queries++ |
||||
stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime |
||||
stat.MethodCalls[recipe.Method]++ |
||||
stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime |
||||
stat.TimeRangeCalls[recipe.RangeCode]++ |
||||
if err != nil { |
||||
return fmt.Errorf("ListInstantMeasures(%d): %s", |
||||
recipe.MetricID, err) |
||||
} |
||||
|
||||
case listCumulativeMeasures: |
||||
t1 := time.Now() |
||||
_, err := conn.ListCumulativeMeasures(proto.ListCumulativeMeasuresReq{ |
||||
MetricID: recipe.MetricID, |
||||
Since: recipe.Since, |
||||
Until: recipe.Until, |
||||
}) |
||||
elapsedTime = time.Since(t1) |
||||
stat.ElapsedTime += elapsedTime |
||||
stat.Queries++ |
||||
stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime |
||||
stat.MethodCalls[recipe.Method]++ |
||||
stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime |
||||
stat.TimeRangeCalls[recipe.RangeCode]++ |
||||
if err != nil { |
||||
return fmt.Errorf("ListCumulativeMeasures(%d): %s", |
||||
recipe.MetricID, err) |
||||
} |
||||
|
||||
case listInstantPeriods: |
||||
t1 := time.Now() |
||||
_, err := conn.ListInstantPeriods(proto.ListInstantPeriodsReq{ |
||||
MetricID: recipe.MetricID, |
||||
Since: recipe.Since, |
||||
Until: recipe.Until, |
||||
GroupBy: recipe.GroupBy, |
||||
AggregateFuncs: diploma.AggregateMin | diploma.AggregateMax | diploma.AggregateAvg, |
||||
}) |
||||
elapsedTime = time.Since(t1) |
||||
stat.ElapsedTime += elapsedTime |
||||
stat.Queries++ |
||||
stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime |
||||
stat.MethodCalls[recipe.Method]++ |
||||
stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime |
||||
stat.TimeRangeCalls[recipe.RangeCode]++ |
||||
if err != nil { |
||||
return fmt.Errorf("ListInstantPeriods(%d): %s", |
||||
recipe.MetricID, err) |
||||
} |
||||
|
||||
case listCumulativePeriods: |
||||
t1 := time.Now() |
||||
_, err := conn.ListCumulativePeriods(proto.ListCumulativePeriodsReq{ |
||||
MetricID: recipe.MetricID, |
||||
Since: recipe.Since, |
||||
Until: recipe.Until, |
||||
GroupBy: recipe.GroupBy, |
||||
}) |
||||
elapsedTime = time.Since(t1) |
||||
stat.ElapsedTime += elapsedTime |
||||
stat.Queries++ |
||||
stat.ElapsedTimeByMethods[recipe.Method] += elapsedTime |
||||
stat.MethodCalls[recipe.Method]++ |
||||
stat.ElapsedTimeByTimeRanges[recipe.RangeCode] += elapsedTime |
||||
stat.TimeRangeCalls[recipe.RangeCode]++ |
||||
if err != nil { |
||||
return fmt.Errorf("ListCumulativePeriods(%d): %s", |
||||
recipe.MetricID, err) |
||||
} |
||||
} |
||||
return |
||||
} |
@ -0,0 +1,261 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"math/rand" |
||||
"os" |
||||
"sync" |
||||
"time" |
||||
|
||||
"gopkg.in/ini.v1" |
||||
"gordenko.dev/dima/diploma/client" |
||||
) |
||||
|
||||
const ( |
||||
listCumulativeMeasures = 0 |
||||
listCumulativePeriods = 1 |
||||
listInstantMeasures = 2 |
||||
listInstantPeriods = 3 |
||||
listCurrentValues = 4 |
||||
|
||||
methodsQty = 5 |
||||
timeRangesQty = 4 |
||||
) |
||||
|
||||
var ( |
||||
methodCodeToName = []string{ |
||||
"listCumulativeMeasures", |
||||
"listCumulativePeriods", |
||||
"listInstantMeasures", |
||||
"listInstantPeriods", |
||||
"listCurrentValues", |
||||
} |
||||
|
||||
rangeCodeToName = []string{ |
||||
"last day", |
||||
"last week", |
||||
"last month", |
||||
"random time range", |
||||
} |
||||
) |
||||
|
||||
type WorkerStat struct { |
||||
Queries int |
||||
ElapsedTime time.Duration |
||||
MethodCalls []int |
||||
ElapsedTimeByMethods []time.Duration |
||||
TimeRangeCalls []int |
||||
ElapsedTimeByTimeRanges []time.Duration |
||||
} |
||||
|
||||
func main() { |
||||
var ( |
||||
iniFileName string |
||||
) |
||||
|
||||
flag.Usage = func() { |
||||
fmt.Fprint(flag.CommandLine.Output(), helpMessage) |
||||
fmt.Fprint(flag.CommandLine.Output(), configExample) |
||||
fmt.Fprintf(flag.CommandLine.Output(), mainUsage, os.Args[0]) |
||||
flag.PrintDefaults() |
||||
} |
||||
flag.StringVar(&iniFileName, "c", "loadtest.ini", "path to *.ini config file") |
||||
flag.Parse() |
||||
|
||||
config, err := loadConfig(iniFileName) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
rand.Seed(time.Now().UnixNano()) |
||||
|
||||
metrics, err := readMetricInfo(config.MetricsInfo) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
var ( |
||||
wg = new(sync.WaitGroup) |
||||
stats = make([]*WorkerStat, config.Connections) |
||||
queryGenerator = NewRandomQueryGenerator(RandomQueryGeneratorOptions{ |
||||
Metrics: metrics, |
||||
// call method probabilitites
|
||||
ListCurrentValuesProbability: 50, // current values / others
|
||||
ListPeriodsProbability: 80, // periods / measures
|
||||
// time range probabilities
|
||||
TimeRangeProbabilities: []int{ |
||||
82, // last day
|
||||
12, // last week
|
||||
3, // last month
|
||||
3, // any range
|
||||
}, |
||||
}) |
||||
) |
||||
|
||||
for i := range stats { |
||||
stats[i] = &WorkerStat{ |
||||
MethodCalls: make([]int, methodsQty), |
||||
ElapsedTimeByMethods: make([]time.Duration, methodsQty), |
||||
TimeRangeCalls: make([]int, timeRangesQty), |
||||
ElapsedTimeByTimeRanges: make([]time.Duration, timeRangesQty), |
||||
} |
||||
} |
||||
|
||||
t1 := time.Now() |
||||
|
||||
for i := range config.Connections { |
||||
wg.Add(1) |
||||
go func(stat *WorkerStat) { |
||||
defer wg.Done() |
||||
|
||||
conn, err := client.Connect(config.DatabaseAddr) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
defer conn.Close() |
||||
|
||||
for range config.RequestsPerConn { |
||||
err := execQuery(conn, queryGenerator, stat) |
||||
if err != nil { |
||||
log.Println(err) |
||||
} |
||||
} |
||||
}(stats[i]) |
||||
} |
||||
|
||||
wg.Wait() |
||||
|
||||
testingTime := time.Since(t1) |
||||
|
||||
var ( |
||||
methodCalls = make([]int, methodsQty) |
||||
elapsedTimeByMethods = make([]time.Duration, methodsQty) |
||||
timeRangeCalls = make([]int, timeRangesQty) |
||||
elapsedTimeByTimeRanges = make([]time.Duration, timeRangesQty) |
||||
totalElapsedTime time.Duration |
||||
totalQueries int |
||||
avgTimePerQuery time.Duration |
||||
rps float64 |
||||
) |
||||
|
||||
for _, stat := range stats { |
||||
totalElapsedTime += stat.ElapsedTime |
||||
totalQueries += stat.Queries |
||||
|
||||
for i, elapsedTime := range stat.ElapsedTimeByMethods { |
||||
elapsedTimeByMethods[i] += elapsedTime |
||||
} |
||||
for i, qty := range stat.MethodCalls { |
||||
methodCalls[i] += qty |
||||
} |
||||
for i, elapsedTime := range stat.ElapsedTimeByTimeRanges { |
||||
elapsedTimeByTimeRanges[i] += elapsedTime |
||||
} |
||||
for i, qty := range stat.TimeRangeCalls { |
||||
timeRangeCalls[i] += qty |
||||
} |
||||
} |
||||
|
||||
avgTimePerQuery = totalElapsedTime / time.Duration(totalQueries) |
||||
rps = float64(config.Connections*config.RequestsPerConn) / testingTime.Seconds() |
||||
|
||||
fmt.Printf(`TEST RESULTS: |
||||
Time: %.0f seconds |
||||
Connections: %d |
||||
Requests per conn: %d |
||||
Total requests: %d |
||||
AVG request time: %v |
||||
RPS: %d |
||||
|
||||
`, |
||||
testingTime.Seconds(), config.Connections, config.RequestsPerConn, |
||||
totalQueries, avgTimePerQuery, int(rps)) |
||||
|
||||
for i, calls := range methodCalls { |
||||
totalElapsedTimeByMethod := elapsedTimeByMethods[i] |
||||
|
||||
methodPercent := float64(calls*100) / float64(totalQueries) |
||||
|
||||
fmt.Printf("%s: %d (%.1f%%), AVG request time: %v\n", |
||||
methodCodeToName[i], calls, methodPercent, |
||||
totalElapsedTimeByMethod/time.Duration(calls)) |
||||
} |
||||
|
||||
fmt.Println() |
||||
|
||||
for i, calls := range timeRangeCalls { |
||||
totalElapsedTimeByTimeRange := elapsedTimeByTimeRanges[i] |
||||
|
||||
timeRangePercent := float64(calls*100) / float64(totalQueries-methodCalls[listCurrentValues]) |
||||
|
||||
fmt.Printf("%s: %d (%.1f%%), AVG request time: %v\n", |
||||
rangeCodeToName[i], calls, timeRangePercent, |
||||
totalElapsedTimeByTimeRange/time.Duration(calls)) |
||||
} |
||||
} |
||||
|
||||
// CONFIG FILE
|
||||
|
||||
const mainUsage = `Usage: |
||||
%s -c path/to/config.ini |
||||
|
||||
` |
||||
|
||||
const helpMessage = `Diploma project. Load test. Version: 1.0 |
||||
created by Dmytro Gordenko, 1.e4.kc6@gmail.com |
||||
` |
||||
|
||||
const configExample = ` |
||||
loadtest.ini example: |
||||
|
||||
databaseAddr = :12345 |
||||
metricsInfo = ../../datadir/metrics.info |
||||
connections = 1000 |
||||
requestsPerConn = 500 |
||||
|
||||
` |
||||
|
||||
type Config struct { |
||||
DatabaseAddr string |
||||
MetricsInfo string |
||||
Connections int |
||||
RequestsPerConn int |
||||
} |
||||
|
||||
func (s Config) String() string { |
||||
return fmt.Sprintf(`starting options: |
||||
databaseAddr = %s |
||||
metricsInfo = %s |
||||
connections = %d |
||||
requestsPerConn = %d |
||||
`, |
||||
s.DatabaseAddr, s.MetricsInfo, s.Connections, s.RequestsPerConn) |
||||
} |
||||
|
||||
func loadConfig(iniFileName string) (_ Config, err error) { |
||||
file, err := ini.Load(iniFileName) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
conf := Config{} |
||||
|
||||
top := file.Section("") |
||||
|
||||
conf.DatabaseAddr = top.Key("databaseAddr").String() |
||||
conf.MetricsInfo = top.Key("metricsInfo").String() |
||||
|
||||
conf.Connections, err = top.Key("connections").Int() |
||||
if err != nil { |
||||
err = fmt.Errorf("'connections' option is required in config file") |
||||
return |
||||
} |
||||
conf.RequestsPerConn, err = top.Key("requestsPerConn").Int() |
||||
if err != nil { |
||||
err = fmt.Errorf("'requestsPerConn' option is required in config file") |
||||
return |
||||
} |
||||
return conf, nil |
||||
} |
@ -0,0 +1,81 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"math/rand" |
||||
"time" |
||||
|
||||
"gordenko.dev/dima/diploma/client" |
||||
) |
||||
|
||||
func GenerateCumulativeMeasures(days int) []client.Measure { |
||||
var ( |
||||
measures []client.Measure |
||||
minutes = []int{14, 29, 44, 59} |
||||
hoursPerDay = 24 |
||||
totalHours = days * hoursPerDay |
||||
since = time.Now().AddDate(0, 0, -days) |
||||
totalValue float64 |
||||
) |
||||
|
||||
for i := range totalHours { |
||||
hourTime := since.Add(time.Duration(i) * time.Hour) |
||||
for _, m := range minutes { |
||||
measureTime := time.Date( |
||||
hourTime.Year(), |
||||
hourTime.Month(), |
||||
hourTime.Day(), |
||||
hourTime.Hour(), |
||||
m, // minutes
|
||||
0, // seconds
|
||||
0, // nanoseconds
|
||||
time.Local, |
||||
) |
||||
|
||||
measure := client.Measure{ |
||||
Timestamp: uint32(measureTime.Unix()), |
||||
Value: totalValue, |
||||
} |
||||
measures = append(measures, measure) |
||||
|
||||
totalValue += rand.Float64() |
||||
} |
||||
} |
||||
return measures |
||||
} |
||||
|
||||
func GenerateInstantMeasures(days int, baseValue float64) []client.Measure { |
||||
var ( |
||||
measures []client.Measure |
||||
minutes = []int{14, 29, 44, 59} |
||||
hoursPerDay = 24 |
||||
totalHours = days * hoursPerDay |
||||
since = time.Now().AddDate(0, 0, -days) |
||||
) |
||||
|
||||
for i := range totalHours { |
||||
hourTime := since.Add(time.Duration(i) * time.Hour) |
||||
for _, m := range minutes { |
||||
measureTime := time.Date( |
||||
hourTime.Year(), |
||||
hourTime.Month(), |
||||
hourTime.Day(), |
||||
hourTime.Hour(), |
||||
m, // minutes
|
||||
0, // seconds
|
||||
0, // nanoseconds
|
||||
time.Local, |
||||
) |
||||
|
||||
// value = +-10% from base value
|
||||
fluctuation := baseValue * 0.1 |
||||
value := baseValue + (rand.Float64()*2-1)*fluctuation |
||||
|
||||
measure := client.Measure{ |
||||
Timestamp: uint32(measureTime.Unix()), |
||||
Value: value, |
||||
} |
||||
measures = append(measures, measure) |
||||
} |
||||
} |
||||
return measures |
||||
} |
@ -0,0 +1,90 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"log" |
||||
"os" |
||||
|
||||
"gopkg.in/ini.v1" |
||||
"gordenko.dev/dima/diploma/client" |
||||
) |
||||
|
||||
var ( |
||||
metricTypeToName = []string{ |
||||
"", |
||||
"cumulative", |
||||
"instant", |
||||
} |
||||
) |
||||
|
||||
func main() { |
||||
var ( |
||||
iniFileName string |
||||
) |
||||
|
||||
flag.Usage = func() { |
||||
fmt.Fprint(flag.CommandLine.Output(), helpMessage) |
||||
fmt.Fprint(flag.CommandLine.Output(), configExample) |
||||
fmt.Fprintf(flag.CommandLine.Output(), mainUsage, os.Args[0]) |
||||
flag.PrintDefaults() |
||||
} |
||||
flag.StringVar(&iniFileName, "c", "requests.ini", "path to *.ini config file") |
||||
flag.Parse() |
||||
|
||||
config, err := loadConfig(iniFileName) |
||||
if err != nil { |
||||
log.Fatalln(err) |
||||
} |
||||
|
||||
conn, err := client.Connect(config.DatabaseAddr) |
||||
if err != nil { |
||||
log.Fatalf("client.Connect(%s): %s\n", config.DatabaseAddr, err) |
||||
} else { |
||||
fmt.Println("Connected to database") |
||||
} |
||||
|
||||
sendRequests(conn) |
||||
} |
||||
|
||||
// CONFIG FILE
|
||||
|
||||
const mainUsage = `Usage: |
||||
%s -c path/to/config.ini |
||||
|
||||
` |
||||
|
||||
const helpMessage = `Diploma project. Example requests. Version: 1.0 |
||||
created by Dmytro Gordenko, 1.e4.kc6@gmail.com |
||||
` |
||||
|
||||
const configExample = ` |
||||
requests.ini example: |
||||
|
||||
databaseAddr = :12345 |
||||
|
||||
` |
||||
|
||||
type Config struct { |
||||
DatabaseAddr string |
||||
} |
||||
|
||||
func (s Config) String() string { |
||||
return fmt.Sprintf(`starting options: |
||||
databaseAddr = %s |
||||
`, |
||||
s.DatabaseAddr) |
||||
} |
||||
|
||||
func loadConfig(iniFileName string) (_ Config, err error) { |
||||
file, err := ini.Load(iniFileName) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
conf := Config{} |
||||
top := file.Section("") |
||||
|
||||
conf.DatabaseAddr = top.Key("databaseAddr").String() |
||||
return conf, nil |
||||
} |
@ -0,0 +1,361 @@ |
||||
package main |
||||
|
||||
import ( |
||||
"fmt" |
||||
"log" |
||||
"time" |
||||
|
||||
"gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/client" |
||||
"gordenko.dev/dima/diploma/proto" |
||||
) |
||||
|
||||
func sendRequests(conn *client.Connection) { |
||||
var ( |
||||
instantMetricID uint32 = 10000 |
||||
cumulativeMetricID uint32 = 10001 |
||||
fracDigits byte = 2 |
||||
err error |
||||
) |
||||
|
||||
conn.DeleteMetric(instantMetricID) |
||||
conn.DeleteMetric(cumulativeMetricID) |
||||
|
||||
// ADD INSTANT METRIC
|
||||
|
||||
err = conn.AddMetric(client.Metric{ |
||||
MetricID: instantMetricID, |
||||
MetricType: diploma.Instant, |
||||
FracDigits: fracDigits, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.AddMetric: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nInstant metric %d added\n", instantMetricID) |
||||
} |
||||
|
||||
// GET INSTANT METRIC
|
||||
|
||||
iMetric, err := conn.GetMetric(instantMetricID) |
||||
if err != nil { |
||||
log.Fatalf("conn.GetMetric: %s\n", err) |
||||
} else { |
||||
fmt.Printf(` |
||||
GetMetric: |
||||
metricID: %d |
||||
metricType: %s |
||||
fracDigits: %d |
||||
`, |
||||
iMetric.MetricID, metricTypeToName[iMetric.MetricType], fracDigits) |
||||
} |
||||
|
||||
// APPEND MEASURES
|
||||
|
||||
instantMeasures := GenerateInstantMeasures(62, 220) |
||||
|
||||
err = conn.AppendMeasures(client.AppendMeasuresReq{ |
||||
MetricID: instantMetricID, |
||||
Measures: instantMeasures, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.AppendMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nAppended %d measures for the metric %d\n", |
||||
len(instantMeasures), instantMetricID) |
||||
} |
||||
|
||||
// LIST INSTANT MEASURES
|
||||
|
||||
lastTimestamp := instantMeasures[len(instantMeasures)-1].Timestamp |
||||
until := time.Unix(int64(lastTimestamp), 0) |
||||
since := until.Add(-5 * time.Hour) |
||||
|
||||
instantList, err := conn.ListInstantMeasures(proto.ListInstantMeasuresReq{ |
||||
MetricID: instantMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListInstantMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListInstantMeasures %s - %s:\n", |
||||
formatTime(uint32(since.Unix())), formatTime(uint32(until.Unix()))) |
||||
for _, item := range instantList { |
||||
fmt.Printf(" %s => %.2f\n", formatTime(item.Timestamp), item.Value) |
||||
} |
||||
} |
||||
|
||||
// LIST ALL INSTANT MEASURES
|
||||
|
||||
instantList, err = conn.ListAllInstantMeasures(instantMetricID) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListAllInstantMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListAllInstantMeasures (last 15 items):\n") |
||||
for _, item := range instantList[:15] { |
||||
fmt.Printf(" %s => %.2f\n", formatTime(item.Timestamp), item.Value) |
||||
} |
||||
} |
||||
|
||||
// LIST INSTANT PERIODS (group by hour)
|
||||
|
||||
until = time.Unix(int64(lastTimestamp+1), 0) |
||||
since = until.Add(-24 * time.Hour) |
||||
|
||||
instantPeriods, err := conn.ListInstantPeriods(proto.ListInstantPeriodsReq{ |
||||
MetricID: instantMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
GroupBy: diploma.GroupByHour, |
||||
AggregateFuncs: diploma.AggregateMin | diploma.AggregateMax | diploma.AggregateAvg, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListInstantPeriods: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListInstantPeriods (1 day, group by hour):\n") |
||||
for _, item := range instantPeriods { |
||||
fmt.Printf(" %s => min %.2f, max %.2f, avg %.2f\n", formatHourPeriod(item.Period), item.Min, item.Max, item.Avg) |
||||
} |
||||
} |
||||
|
||||
// LIST INSTANT PERIODS (group by day)
|
||||
|
||||
until = time.Unix(int64(lastTimestamp+1), 0) |
||||
since = until.AddDate(0, 0, -7) |
||||
|
||||
instantPeriods, err = conn.ListInstantPeriods(proto.ListInstantPeriodsReq{ |
||||
MetricID: instantMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
GroupBy: diploma.GroupByDay, |
||||
AggregateFuncs: diploma.AggregateMin | diploma.AggregateMax | diploma.AggregateAvg, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListInstantPeriods: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListInstantPeriods (7 days, group by day):\n") |
||||
for _, item := range instantPeriods { |
||||
fmt.Printf(" %s => min %.2f, max %.2f, avg %.2f\n", formatDayPeriod(item.Period), item.Min, item.Max, item.Avg) |
||||
} |
||||
} |
||||
|
||||
// LIST INSTANT PERIODS (group by month)
|
||||
|
||||
until = time.Unix(int64(lastTimestamp+1), 0) |
||||
since = until.AddDate(0, 0, -62) |
||||
|
||||
instantPeriods, err = conn.ListInstantPeriods(proto.ListInstantPeriodsReq{ |
||||
MetricID: instantMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
GroupBy: diploma.GroupByMonth, |
||||
AggregateFuncs: diploma.AggregateMin | diploma.AggregateMax | diploma.AggregateAvg, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListInstantPeriods: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListInstantPeriods (62 days, group by month):\n") |
||||
for _, item := range instantPeriods { |
||||
fmt.Printf(" %s => min %.2f, max %.2f, avg %.2f\n", formatMonthPeriod(item.Period), item.Min, item.Max, item.Avg) |
||||
} |
||||
} |
||||
|
||||
// DELETE INSTANT METRIC MEASURES
|
||||
|
||||
err = conn.DeleteMeasures(proto.DeleteMeasuresReq{ |
||||
MetricID: instantMetricID, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.DeleteMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nInstant metric %d measures deleted\n", instantMetricID) |
||||
} |
||||
|
||||
// DELETE INSTANT METRIC
|
||||
|
||||
err = conn.DeleteMetric(instantMetricID) |
||||
if err != nil { |
||||
log.Fatalf("conn.DeleteMetric: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nInstant metric %d deleted\n", instantMetricID) |
||||
} |
||||
|
||||
// ADD CUMULATIVE METRIC
|
||||
|
||||
err = conn.AddMetric(client.Metric{ |
||||
MetricID: cumulativeMetricID, |
||||
MetricType: diploma.Cumulative, |
||||
FracDigits: fracDigits, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.AddMetric: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nCumulative metric %d added\n", cumulativeMetricID) |
||||
} |
||||
|
||||
// GET CUMULATIVE METRIC
|
||||
|
||||
cMetric, err := conn.GetMetric(cumulativeMetricID) |
||||
if err != nil { |
||||
log.Fatalf("conn.GetMetric: %s\n", err) |
||||
} else { |
||||
fmt.Printf(` |
||||
GetMetric: |
||||
metricID: %d |
||||
metricType: %s |
||||
fracDigits: %d |
||||
`, |
||||
cMetric.MetricID, metricTypeToName[cMetric.MetricType], fracDigits) |
||||
} |
||||
|
||||
// APPEND MEASURES
|
||||
|
||||
cumulativeMeasures := GenerateCumulativeMeasures(62) |
||||
|
||||
err = conn.AppendMeasures(client.AppendMeasuresReq{ |
||||
MetricID: cumulativeMetricID, |
||||
Measures: cumulativeMeasures, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.AppendMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nAppended %d measures for the metric %d\n", |
||||
len(cumulativeMeasures), cumulativeMetricID) |
||||
} |
||||
|
||||
// LIST CUMULATIVE MEASURES
|
||||
|
||||
lastTimestamp = cumulativeMeasures[len(cumulativeMeasures)-1].Timestamp |
||||
until = time.Unix(int64(lastTimestamp), 0) |
||||
since = until.Add(-5 * time.Hour) |
||||
|
||||
cumulativeList, err := conn.ListCumulativeMeasures(proto.ListCumulativeMeasuresReq{ |
||||
MetricID: cumulativeMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListCumulativeMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListCumulativeMeasures %s - %s:\n", |
||||
formatTime(uint32(since.Unix())), formatTime(uint32(until.Unix()))) |
||||
|
||||
for _, item := range cumulativeList { |
||||
fmt.Printf(" %s => %.2f\n", formatTime(item.Timestamp), item.Value) |
||||
} |
||||
} |
||||
|
||||
// LIST ALL CUMULATIVE MEASURES
|
||||
|
||||
cumulativeList, err = conn.ListAllCumulativeMeasures(cumulativeMetricID) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListAllCumulativeMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListAllCumulativeMeasures (last 15 items):\n") |
||||
for _, item := range cumulativeList[:15] { |
||||
fmt.Printf(" %s => %.2f\n", formatTime(item.Timestamp), item.Value) |
||||
} |
||||
} |
||||
|
||||
// LIST CUMULATIVE PERIODS (group by hour)
|
||||
|
||||
until = time.Unix(int64(lastTimestamp+1), 0) |
||||
since = until.Add(-24 * time.Hour) |
||||
|
||||
cumulativePeriods, err := conn.ListCumulativePeriods(proto.ListCumulativePeriodsReq{ |
||||
MetricID: cumulativeMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
GroupBy: diploma.GroupByHour, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListCumulativePeriods: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListCumulativePeriods (1 day, group by hour):\n") |
||||
for _, item := range cumulativePeriods { |
||||
fmt.Printf(" %s => end value %.2f, total %.2f\n", formatHourPeriod(item.Period), item.EndValue, item.Total) |
||||
} |
||||
} |
||||
|
||||
// LIST CUMULATIVE PERIODS (group by day)
|
||||
|
||||
until = time.Unix(int64(lastTimestamp+1), 0) |
||||
since = until.AddDate(0, 0, -7) |
||||
|
||||
cumulativePeriods, err = conn.ListCumulativePeriods(proto.ListCumulativePeriodsReq{ |
||||
MetricID: cumulativeMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
GroupBy: diploma.GroupByDay, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListCumulativePeriods: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListCumulativePeriods (7 days, group by day):\n") |
||||
for _, item := range cumulativePeriods { |
||||
fmt.Printf(" %s => end value %.2f, total %.2f\n", formatDayPeriod(item.Period), item.EndValue, item.Total) |
||||
} |
||||
} |
||||
|
||||
// LIST CUMULATIVE PERIODS (group by day)
|
||||
|
||||
until = time.Unix(int64(lastTimestamp+1), 0) |
||||
since = until.AddDate(0, 0, -62) |
||||
|
||||
cumulativePeriods, err = conn.ListCumulativePeriods(proto.ListCumulativePeriodsReq{ |
||||
MetricID: cumulativeMetricID, |
||||
Since: uint32(since.Unix()), |
||||
Until: uint32(until.Unix()), |
||||
GroupBy: diploma.GroupByMonth, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.ListCumulativePeriods: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nListCumulativePeriods (62 days, group by month):\n") |
||||
for _, item := range cumulativePeriods { |
||||
fmt.Printf(" %s => end value %.2f, total %.2f\n", formatMonthPeriod(item.Period), item.EndValue, item.Total) |
||||
} |
||||
} |
||||
|
||||
// DELETE CUMULATIVE METRIC MEASURES
|
||||
|
||||
err = conn.DeleteMeasures(proto.DeleteMeasuresReq{ |
||||
MetricID: cumulativeMetricID, |
||||
}) |
||||
if err != nil { |
||||
log.Fatalf("conn.DeleteMeasures: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nCumulative metric %d measures deleted\n", cumulativeMetricID) |
||||
} |
||||
|
||||
// DELETE CUMULATIVE METRIC
|
||||
|
||||
err = conn.DeleteMetric(cumulativeMetricID) |
||||
if err != nil { |
||||
log.Fatalf("conn.DeleteMetric: %s\n", err) |
||||
} else { |
||||
fmt.Printf("\nCumulative metric %d deleted\n", cumulativeMetricID) |
||||
} |
||||
} |
||||
|
||||
const datetimeLayout = "2006-01-02 15:04:05" |
||||
|
||||
func formatTime(timestamp uint32) string { |
||||
tm := time.Unix(int64(timestamp), 0) |
||||
return tm.Format(datetimeLayout) |
||||
} |
||||
|
||||
func formatHourPeriod(period uint32) string { |
||||
tm := time.Unix(int64(period), 0) |
||||
return tm.Format("2006-01-02 15:00 - 15") + ":59" |
||||
} |
||||
|
||||
func formatDayPeriod(period uint32) string { |
||||
tm := time.Unix(int64(period), 0) |
||||
return tm.Format("2006-01-02") |
||||
} |
||||
|
||||
func formatMonthPeriod(period uint32) string { |
||||
tm := time.Unix(int64(period), 0) |
||||
return tm.Format("2006-01") |
||||
} |
@ -0,0 +1,72 @@ |
||||
package freelist |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sync" |
||||
|
||||
"github.com/RoaringBitmap/roaring/v2" |
||||
) |
||||
|
||||
type FreeList struct { |
||||
mutex sync.Mutex |
||||
free *roaring.Bitmap |
||||
reserved *roaring.Bitmap |
||||
} |
||||
|
||||
func New() *FreeList { |
||||
return &FreeList{ |
||||
free: roaring.New(), |
||||
reserved: roaring.New(), |
||||
} |
||||
} |
||||
|
||||
func (s *FreeList) Restore(serialized []byte) error { |
||||
err := s.free.UnmarshalBinary(serialized) |
||||
if err != nil { |
||||
return fmt.Errorf("UnmarshalBinary: %s", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
func (s *FreeList) AddPages(pageNumbers []uint32) { |
||||
if len(pageNumbers) == 0 { |
||||
return |
||||
} |
||||
s.mutex.Lock() |
||||
s.free.AddMany(pageNumbers) |
||||
s.mutex.Unlock() |
||||
} |
||||
|
||||
// ReserveDataPage - аллокатор резервирует страницу, но не удаляет до визова
|
||||
// DeleteFromFree, ибо транзакция может не завершится, а между віделением страници
|
||||
// и падением транзакции - будет создан init файл.
|
||||
func (s *FreeList) ReservePage() (pageNo uint32) { |
||||
s.mutex.Lock() |
||||
defer s.mutex.Unlock() |
||||
|
||||
if s.free.IsEmpty() { |
||||
return |
||||
} |
||||
pageNo = s.free.Minimum() |
||||
s.free.Remove(pageNo) |
||||
s.reserved.Add(pageNo) |
||||
return |
||||
} |
||||
|
||||
// Удаляет ранее зарезервированные страницы
|
||||
func (s *FreeList) DeletePages(pageNumbers []uint32) { |
||||
s.mutex.Lock() |
||||
for _, pageNo := range pageNumbers { |
||||
s.reserved.Remove(pageNo) |
||||
s.free.Remove(pageNo) // прокрута TransactionLog
|
||||
} |
||||
s.mutex.Unlock() |
||||
} |
||||
|
||||
func (s *FreeList) Serialize() ([]byte, error) { |
||||
s.mutex.Lock() |
||||
defer s.mutex.Unlock() |
||||
tmp := roaring.Or(s.free, s.reserved) |
||||
tmp.RunOptimize() |
||||
return tmp.ToBytes() |
||||
} |
@ -0,0 +1,13 @@ |
||||
module gordenko.dev/dima/diploma |
||||
|
||||
go 1.24.0 |
||||
|
||||
require ( |
||||
github.com/RoaringBitmap/roaring/v2 v2.5.0 |
||||
gopkg.in/ini.v1 v1.67.0 |
||||
) |
||||
|
||||
require ( |
||||
github.com/bits-and-blooms/bitset v1.12.0 // indirect |
||||
github.com/mschoch/smat v0.2.0 // indirect |
||||
) |
@ -0,0 +1,20 @@ |
||||
github.com/RoaringBitmap/roaring/v2 v2.5.0 h1:TJ45qCM7D7fIEBwKd9zhoR0/S1egfnSSIzLU1e1eYLY= |
||||
github.com/RoaringBitmap/roaring/v2 v2.5.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0= |
||||
github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= |
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= |
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= |
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= |
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= |
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= |
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= |
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= |
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= |
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= |
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= |
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
@ -0,0 +1,11 @@ |
||||
cd examples/database |
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../../database_linux |
||||
cd - |
||||
|
||||
cd examples/loadtest |
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../../loadtest_linux |
||||
cd - |
||||
|
||||
cd examples/requests |
||||
env CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../../requests_linux |
||||
cd - |
@ -0,0 +1,9 @@ |
||||
|
||||
# host:port |
||||
databaseAddr = :12345 |
||||
# path to metrics.info file |
||||
metricsInfo = testdir/metrics.info |
||||
# the number of concurrently open connections. |
||||
connections = 1000 |
||||
# send requests per one connection |
||||
requestsPerConn = 500 |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,473 @@ |
||||
package proto |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/bufreader" |
||||
) |
||||
|
||||
const ( |
||||
TypeDeleteMeasures byte = 1 |
||||
TypeListCurrentValues byte = 2 |
||||
TypeListInstantMeasures byte = 3 |
||||
TypeListCumulativeMeasures byte = 33 |
||||
TypeListInstantPeriods byte = 4 |
||||
TypeListCumulativePeriods byte = 44 |
||||
TypeGetMetric byte = 5 |
||||
TypeAddMetric byte = 6 |
||||
TypeListAllInstantMeasures byte = 8 |
||||
TypeListAllCumulativeMeasures byte = 88 |
||||
TypeRangeTotal byte = 9 |
||||
TypeAppendMeasure byte = 10 |
||||
TypeAppendMeasures byte = 11 |
||||
TypeDeleteMetric byte = 12 |
||||
|
||||
RespPartOfValue byte = 255 |
||||
RespEndOfValue byte = 254 |
||||
RespError byte = 253 |
||||
RespSuccess byte = 252 |
||||
RespValue byte = 251 |
||||
|
||||
ErrNoMetric = 1 |
||||
ErrDuplicate = 2 |
||||
ErrWrongMetricType = 3 |
||||
ErrWrongFracDigits = 4 |
||||
ErrExpiredMeasure = 5 |
||||
ErrNonMonotonicValue = 6 |
||||
ErrEmptyMetricID = 7 |
||||
ErrInvalidRange = 8 |
||||
ErrUnexpected = 9 |
||||
) |
||||
|
||||
func ErrorCodeToText(code uint16) string { |
||||
switch code { |
||||
case ErrNoMetric: |
||||
return "NoMetric" |
||||
case ErrDuplicate: |
||||
return "Duplicate" |
||||
case ErrWrongMetricType: |
||||
return "WrongMetricType" |
||||
case ErrWrongFracDigits: |
||||
return "WrongFracDigits" |
||||
case ErrExpiredMeasure: |
||||
return "ExpiredMeasure" |
||||
case ErrNonMonotonicValue: |
||||
return "NonMonotonicValue" |
||||
case ErrEmptyMetricID: |
||||
return "EmptyMetricID" |
||||
case ErrInvalidRange: |
||||
return "InvalidRange" |
||||
case ErrUnexpected: |
||||
return "Unexpected" |
||||
default: |
||||
return "" |
||||
} |
||||
} |
||||
|
||||
type GetMetricReq struct { |
||||
MetricID uint32 |
||||
} |
||||
|
||||
type ListCurrentValuesReq struct { |
||||
MetricIDs []uint32 |
||||
} |
||||
|
||||
type AddMetricReq struct { |
||||
MetricID uint32 |
||||
MetricType octopus.MetricType |
||||
FracDigits int |
||||
} |
||||
|
||||
type UpdateMetricReq struct { |
||||
MetricID uint32 |
||||
MetricType octopus.MetricType |
||||
FracDigits int |
||||
} |
||||
|
||||
type DeleteMetricReq struct { |
||||
MetricID uint32 |
||||
} |
||||
|
||||
type DeleteMeasuresReq struct { |
||||
MetricID uint32 |
||||
Since uint32 // timestamp (optional)
|
||||
} |
||||
|
||||
type AppendMeasureReq struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
type ListAllInstantMetricMeasuresReq struct { |
||||
MetricID uint32 |
||||
} |
||||
|
||||
type ListAllCumulativeMeasuresReq struct { |
||||
MetricID uint32 |
||||
} |
||||
|
||||
type ListInstantMeasuresReq struct { |
||||
MetricID uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
FirstHourOfDay int |
||||
} |
||||
|
||||
type ListCumulativeMeasuresReq struct { |
||||
MetricID uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
FirstHourOfDay int |
||||
} |
||||
|
||||
type ListInstantPeriodsReq struct { |
||||
MetricID uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
GroupBy octopus.GroupBy |
||||
AggregateFuncs byte |
||||
FirstHourOfDay int |
||||
LastDayOfMonth int |
||||
} |
||||
|
||||
type ListCumulativePeriodsReq struct { |
||||
MetricID uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
GroupBy octopus.GroupBy |
||||
FirstHourOfDay int |
||||
LastDayOfMonth int |
||||
} |
||||
|
||||
type Metric struct { |
||||
MetricID uint32 |
||||
MetricType octopus.MetricType |
||||
FracDigits int |
||||
} |
||||
|
||||
type RangeTotalReq struct { |
||||
MetricID uint32 |
||||
Since uint32 |
||||
Until uint32 |
||||
} |
||||
|
||||
func PackAddMetricReq(req AddMetricReq) []byte { |
||||
arr := []byte{ |
||||
TypeAddMetric, |
||||
0, 0, 0, 0, //
|
||||
byte(req.MetricType), |
||||
byte(req.FracDigits), |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
return arr |
||||
} |
||||
|
||||
func PackDeleteMetricReq(req DeleteMetricReq) []byte { |
||||
arr := []byte{ |
||||
TypeDeleteMetric, |
||||
0, 0, 0, 0, // metricID
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
return arr |
||||
} |
||||
|
||||
func PackAppendMeasure(req AppendMeasureReq) []byte { |
||||
arr := []byte{ |
||||
TypeAppendMeasure, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // timestamp
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // value
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], uint32(req.Timestamp)) |
||||
bin.PutFloat64(arr[9:], req.Value) |
||||
return arr |
||||
} |
||||
|
||||
func PackDeleteMeasuresReq(req DeleteMeasuresReq) []byte { |
||||
arr := []byte{ |
||||
TypeDeleteMeasures, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // since
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], uint32(req.Since)) |
||||
return arr |
||||
} |
||||
|
||||
// UNPACK reqs
|
||||
|
||||
func UnpackAddMetricReq(arr []byte) (m AddMetricReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
m.MetricType = octopus.MetricType(arr[4]) |
||||
m.FracDigits = int(arr[5]) |
||||
return |
||||
} |
||||
|
||||
func UnpackUpdateMetricReq(arr []byte) (m UpdateMetricReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
m.MetricType = octopus.MetricType(arr[4]) |
||||
m.FracDigits = int(arr[5]) |
||||
return |
||||
} |
||||
|
||||
func UnpackDeleteMetricReq(arr []byte) (m DeleteMetricReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
return |
||||
} |
||||
|
||||
func UnpackAppendMeasureReq(arr []byte) (m AppendMeasureReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
m.Timestamp = bin.GetUint32(arr[4:]) |
||||
m.Value = bin.GetFloat64(arr[8:]) |
||||
return |
||||
} |
||||
|
||||
func UnpackDeleteMeasuresReq(arr []byte) (m DeleteMeasuresReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
m.Since = bin.GetUint32(arr[4:]) |
||||
return |
||||
} |
||||
|
||||
func UnpackListInstantMeasuresReq(arr []byte) (m ListInstantMeasuresReq) { |
||||
m.MetricID = bin.GetUint32(arr[0:]) |
||||
m.Since = bin.GetUint32(arr[4:]) |
||||
m.Until = bin.GetUint32(arr[8:]) |
||||
m.FirstHourOfDay = int(arr[12]) |
||||
return |
||||
} |
||||
|
||||
func UnpackListCumulativeMeasuresReq(arr []byte) (m ListCumulativeMeasuresReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
m.Since = bin.GetUint32(arr[4:]) |
||||
m.Until = bin.GetUint32(arr[8:]) |
||||
m.FirstHourOfDay = int(arr[12]) |
||||
return |
||||
} |
||||
|
||||
func UnpackListInstantPeriodsReq(arr []byte) (m ListInstantPeriodsReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
m.Since = bin.GetUint32(arr[4:]) |
||||
m.Until = bin.GetUint32(arr[8:]) |
||||
m.GroupBy = octopus.GroupBy(arr[12]) |
||||
m.AggregateFuncs = arr[13] |
||||
m.FirstHourOfDay = int(arr[14]) |
||||
m.LastDayOfMonth = int(arr[15]) |
||||
return |
||||
} |
||||
|
||||
func UnpackListCumulativePeriodsReq(arr []byte) (m ListCumulativePeriodsReq) { |
||||
m.MetricID = bin.GetUint32(arr[0:]) |
||||
m.Since = bin.GetUint32(arr[4:]) |
||||
m.Until = bin.GetUint32(arr[8:]) |
||||
m.GroupBy = octopus.GroupBy(arr[12]) |
||||
m.FirstHourOfDay = int(arr[13]) |
||||
m.LastDayOfMonth = int(arr[14]) |
||||
return |
||||
} |
||||
|
||||
func UnpackRangeTotalReq(arr []byte) (m RangeTotalReq) { |
||||
m.MetricID = bin.GetUint32(arr) |
||||
m.Since = bin.GetUint32(arr[4:]) |
||||
m.Until = bin.GetUint32(arr[8:]) |
||||
return |
||||
} |
||||
|
||||
// READ reqs
|
||||
|
||||
func ReadGetMetricReq(r *bufreader.BufferedReader) (m GetMetricReq, err error) { |
||||
m.MetricID, err = bin.ReadUint32(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
func ReadAddMetricReq(r *bufreader.BufferedReader) (m AddMetricReq, err error) { |
||||
arr, err := r.ReadN(6) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackAddMetricReq(arr), nil |
||||
} |
||||
|
||||
func ReadUpdateMetricReq(r *bufreader.BufferedReader) (m UpdateMetricReq, err error) { |
||||
arr, err := r.ReadN(6) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackUpdateMetricReq(arr), nil |
||||
} |
||||
|
||||
func ReadDeleteMetricReq(r *bufreader.BufferedReader) (m DeleteMetricReq, err error) { |
||||
m.MetricID, err = bin.ReadUint32(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
func ReadAppendMeasureReq(r *bufreader.BufferedReader) (m AppendMeasureReq, err error) { |
||||
arr, err := r.ReadN(16) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackAppendMeasureReq(arr), nil |
||||
} |
||||
|
||||
func ReadDeleteMeasuresReq(r *bufreader.BufferedReader) (m DeleteMeasuresReq, err error) { |
||||
arr, err := r.ReadN(8) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackDeleteMeasuresReq(arr), nil |
||||
} |
||||
|
||||
func ReadListAllInstantMeasuresReq(r *bufreader.BufferedReader) (m ListAllInstantMetricMeasuresReq, err error) { |
||||
m.MetricID, err = bin.ReadUint32(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
func ReadListAllCumulativeMeasuresReq(r *bufreader.BufferedReader) (m ListAllCumulativeMeasuresReq, err error) { |
||||
m.MetricID, err = bin.ReadUint32(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return |
||||
} |
||||
|
||||
func ReadListInstantMeasuresReq(r *bufreader.BufferedReader) (m ListInstantMeasuresReq, err error) { |
||||
arr, err := r.ReadN(13) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackListInstantMeasuresReq(arr), nil |
||||
} |
||||
|
||||
func ReadListCumulativeMeasuresReq(r *bufreader.BufferedReader) (m ListCumulativeMeasuresReq, err error) { |
||||
arr, err := r.ReadN(13) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackListCumulativeMeasuresReq(arr), nil |
||||
} |
||||
|
||||
func ReadListInstantPeriodsReq(r *bufreader.BufferedReader) (m ListInstantPeriodsReq, err error) { |
||||
arr, err := r.ReadN(16) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackListInstantPeriodsReq(arr), nil |
||||
} |
||||
|
||||
func ReadListCumulativePeriodsReq(r *bufreader.BufferedReader) (m ListCumulativePeriodsReq, err error) { |
||||
arr, err := r.ReadN(15) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackListCumulativePeriodsReq(arr), nil |
||||
} |
||||
|
||||
func ReadRangeTotalReq(r *bufreader.BufferedReader) (m RangeTotalReq, err error) { |
||||
arr, err := r.ReadN(12) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
return UnpackRangeTotalReq(arr), nil |
||||
} |
||||
|
||||
func ReadListCurrentValuesReq(r *bufreader.BufferedReader) (m ListCurrentValuesReq, err error) { |
||||
qty, err := bin.ReadUint16(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read req: %s", err) |
||||
return |
||||
} |
||||
|
||||
for i := range int(qty) { |
||||
var metricID uint32 |
||||
metricID, err = bin.ReadUint32(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read metricID (#%d): %s", i, err) |
||||
return |
||||
} |
||||
m.MetricIDs = append(m.MetricIDs, metricID) |
||||
} |
||||
return |
||||
} |
||||
|
||||
type AppendMeasuresReq struct { |
||||
MetricID uint32 |
||||
Measures []Measure |
||||
} |
||||
|
||||
type Measure struct { |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
func PackAppendMeasures(req AppendMeasuresReq) []byte { |
||||
if len(req.Measures) > 65535 { |
||||
panic(fmt.Errorf("wrong measures qty: %d", len(req.Measures))) |
||||
} |
||||
var ( |
||||
prefixSize = 7 |
||||
recordSize = 12 |
||||
arr = make([]byte, prefixSize+len(req.Measures)*recordSize) |
||||
) |
||||
arr[0] = TypeAppendMeasures |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint16(arr[5:], uint16(len(req.Measures))) |
||||
pos := prefixSize |
||||
for _, measure := range req.Measures { |
||||
bin.PutUint32(arr[pos:], measure.Timestamp) |
||||
bin.PutFloat64(arr[pos+4:], measure.Value) |
||||
pos += recordSize |
||||
} |
||||
return arr |
||||
} |
||||
|
||||
func ReadAppendMeasuresReq(r *bufreader.BufferedReader) (m AppendMeasuresReq, err error) { |
||||
prefix, err := bin.ReadN(r, 6) // metricID + measures qty
|
||||
if err != nil { |
||||
err = fmt.Errorf("read prefix: %s", err) |
||||
return |
||||
} |
||||
|
||||
m.MetricID = bin.GetUint32(prefix[0:]) |
||||
qty := bin.GetUint16(prefix[4:]) |
||||
|
||||
for i := range int(qty) { |
||||
var measure Measure |
||||
measure.Timestamp, err = bin.ReadUint32(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read timestamp (#%d): %s", i, err) |
||||
return |
||||
} |
||||
measure.Value, err = bin.ReadFloat64(r) |
||||
if err != nil { |
||||
err = fmt.Errorf("read value (#%d): %s", i, err) |
||||
return |
||||
} |
||||
m.Measures = append(m.Measures, measure) |
||||
} |
||||
return |
||||
} |
@ -0,0 +1,255 @@ |
||||
package recovery |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"os" |
||||
"path/filepath" |
||||
"regexp" |
||||
"sort" |
||||
"strconv" |
||||
) |
||||
|
||||
var ( |
||||
reChanges = regexp.MustCompile(`(\d+)\.changes`) |
||||
reSnapshot = regexp.MustCompile(`(\d+)\.snapshot`) |
||||
) |
||||
|
||||
func joinChangesFileName(dir string, logNumber int) string { |
||||
return filepath.Join(dir, fmt.Sprintf("%d.changes", logNumber)) |
||||
} |
||||
|
||||
type RecoveryRecipe struct { |
||||
Snapshot string |
||||
Changes []string |
||||
LogNumber int |
||||
ToDelete []string |
||||
CompleteSnapshot bool // флаг - что нужно завершить создание снапшота
|
||||
} |
||||
|
||||
type RecoveryAdvisor struct { |
||||
dir string |
||||
verifySnapshot func(string) (bool, error) // (fileName) isVerified, error
|
||||
} |
||||
|
||||
type RecoveryAdvisorOptions struct { |
||||
Dir string |
||||
VerifySnapshot func(string) (bool, error) |
||||
} |
||||
|
||||
func NewRecoveryAdvisor(opt RecoveryAdvisorOptions) (*RecoveryAdvisor, error) { |
||||
if opt.Dir == "" { |
||||
return nil, errors.New("Dir option is required") |
||||
} |
||||
if opt.VerifySnapshot == nil { |
||||
return nil, errors.New("VerifySnapshot option is required") |
||||
} |
||||
return &RecoveryAdvisor{ |
||||
dir: opt.Dir, |
||||
verifySnapshot: opt.VerifySnapshot, |
||||
}, nil |
||||
} |
||||
|
||||
type SnapshotChangesPair struct { |
||||
SnapshotFileName string |
||||
ChangesFileName string |
||||
LogNumber int |
||||
} |
||||
|
||||
func (s *RecoveryAdvisor) getSnapshotChangesPairs() (*SnapshotChangesPair, *SnapshotChangesPair, error) { |
||||
var ( |
||||
numSet = make(map[int]bool) |
||||
changesSet = make(map[int]bool) |
||||
snapshotsSet = make(map[int]bool) |
||||
pairs []SnapshotChangesPair |
||||
) |
||||
|
||||
entries, err := os.ReadDir(s.dir) |
||||
if err != nil { |
||||
return nil, nil, err |
||||
} |
||||
|
||||
for _, entry := range entries { |
||||
if entry.Type().IsRegular() { |
||||
baseName := entry.Name() |
||||
groups := reChanges.FindStringSubmatch(baseName) |
||||
if len(groups) == 2 { |
||||
num, _ := strconv.Atoi(groups[1]) |
||||
|
||||
numSet[num] = true |
||||
changesSet[num] = true |
||||
} |
||||
groups = reSnapshot.FindStringSubmatch(baseName) |
||||
if len(groups) == 2 { |
||||
num, _ := strconv.Atoi(groups[1]) |
||||
|
||||
numSet[num] = true |
||||
snapshotsSet[num] = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
for logNumber := range numSet { |
||||
var ( |
||||
snapshotFileName string |
||||
changesFileName string |
||||
) |
||||
if changesSet[logNumber] { |
||||
changesFileName = joinChangesFileName(s.dir, logNumber) |
||||
} |
||||
if snapshotsSet[logNumber] { |
||||
snapshotFileName = filepath.Join(s.dir, fmt.Sprintf("%d.snapshot", logNumber)) |
||||
} |
||||
pairs = append(pairs, SnapshotChangesPair{ |
||||
ChangesFileName: changesFileName, |
||||
SnapshotFileName: snapshotFileName, |
||||
LogNumber: logNumber, |
||||
}) |
||||
} |
||||
|
||||
if len(pairs) == 0 { |
||||
return nil, nil, nil |
||||
} |
||||
|
||||
sort.Slice(pairs, func(i, j int) bool { |
||||
return pairs[i].LogNumber > pairs[j].LogNumber |
||||
}) |
||||
|
||||
pair := pairs[0] |
||||
if pair.ChangesFileName == "" { |
||||
return nil, nil, fmt.Errorf("has %d.shapshot file, but %d.changes file not found", |
||||
pair.LogNumber, pair.LogNumber) |
||||
} |
||||
|
||||
if len(pairs) > 1 { |
||||
prevPair := pairs[1] |
||||
if prevPair.SnapshotFileName == "" && prevPair.LogNumber != 1 { |
||||
return &pair, nil, nil |
||||
} |
||||
|
||||
if prevPair.ChangesFileName == "" && pair.SnapshotFileName == "" { |
||||
return &pair, nil, nil |
||||
} |
||||
return &pair, &prevPair, nil |
||||
} else { |
||||
return &pair, nil, nil |
||||
} |
||||
} |
||||
|
||||
func (s *RecoveryAdvisor) GetRecipe() (*RecoveryRecipe, error) { |
||||
pair, prevPair, err := s.getSnapshotChangesPairs() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if pair == nil { |
||||
return nil, nil |
||||
} |
||||
|
||||
if pair.SnapshotFileName != "" { |
||||
isVerified, err := s.verifySnapshot(pair.SnapshotFileName) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("verifySnapshot %s: %s", |
||||
pair.SnapshotFileName, err) |
||||
} |
||||
|
||||
if isVerified { |
||||
recipe := &RecoveryRecipe{ |
||||
Snapshot: pair.SnapshotFileName, |
||||
Changes: []string{ |
||||
pair.ChangesFileName, |
||||
}, |
||||
LogNumber: pair.LogNumber, |
||||
} |
||||
|
||||
if prevPair != nil { |
||||
if prevPair.ChangesFileName != "" { |
||||
recipe.ToDelete = append(recipe.ToDelete, prevPair.ChangesFileName) |
||||
} |
||||
if prevPair.SnapshotFileName != "" { |
||||
recipe.ToDelete = append(recipe.ToDelete, prevPair.SnapshotFileName) |
||||
} |
||||
} |
||||
return recipe, nil |
||||
} |
||||
if prevPair != nil { |
||||
return s.tryPrevPair(pair, prevPair) |
||||
} |
||||
return nil, fmt.Errorf("%d.shapshot is corrupted", pair.LogNumber) |
||||
} else { |
||||
if prevPair != nil { |
||||
return s.tryPrevPair(pair, prevPair) |
||||
} else { |
||||
if pair.LogNumber == 1 { |
||||
return &RecoveryRecipe{ |
||||
Changes: []string{ |
||||
pair.ChangesFileName, |
||||
}, |
||||
LogNumber: pair.LogNumber, |
||||
}, nil |
||||
} else { |
||||
return nil, fmt.Errorf("%d.snapshot not found", pair.LogNumber) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *RecoveryAdvisor) tryPrevPair(pair, prevPair *SnapshotChangesPair) (*RecoveryRecipe, error) { |
||||
if prevPair.ChangesFileName == "" { |
||||
if pair.SnapshotFileName != "" { |
||||
return nil, fmt.Errorf("%d.shapshot is corrupted and %d.changes not found", |
||||
pair.LogNumber, prevPair.LogNumber) |
||||
} else { |
||||
return nil, fmt.Errorf("%d.changes not found", prevPair.LogNumber) |
||||
} |
||||
} |
||||
|
||||
if prevPair.SnapshotFileName == "" { |
||||
if prevPair.LogNumber == 1 { |
||||
recipe := &RecoveryRecipe{ |
||||
Changes: []string{ |
||||
prevPair.ChangesFileName, |
||||
pair.ChangesFileName, |
||||
}, |
||||
LogNumber: pair.LogNumber, |
||||
CompleteSnapshot: true, |
||||
ToDelete: []string{ |
||||
prevPair.ChangesFileName, |
||||
}, |
||||
} |
||||
return recipe, nil |
||||
} else { |
||||
if pair.SnapshotFileName != "" { |
||||
return nil, fmt.Errorf("%d.shapshot is corrupted and %d.snapshot not found", |
||||
pair.LogNumber, prevPair.LogNumber) |
||||
} else { |
||||
return nil, fmt.Errorf("%d.snapshot not found", pair.LogNumber) |
||||
} |
||||
} |
||||
} |
||||
|
||||
isVerified, err := s.verifySnapshot(prevPair.SnapshotFileName) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("verifySnapshot %s: %s", |
||||
prevPair.SnapshotFileName, err) |
||||
} |
||||
|
||||
if !isVerified { |
||||
return nil, fmt.Errorf("%d.shapshot is corrupted", prevPair.LogNumber) |
||||
} |
||||
|
||||
recipe := &RecoveryRecipe{ |
||||
Snapshot: prevPair.SnapshotFileName, |
||||
Changes: []string{ |
||||
prevPair.ChangesFileName, |
||||
pair.ChangesFileName, |
||||
}, |
||||
LogNumber: pair.LogNumber, |
||||
CompleteSnapshot: true, |
||||
ToDelete: []string{ |
||||
prevPair.ChangesFileName, |
||||
prevPair.SnapshotFileName, |
||||
}, |
||||
} |
||||
return recipe, nil |
||||
} |
@ -0,0 +1 @@ |
||||
databaseAddr = :12345 |
Binary file not shown.
Binary file not shown.
@ -0,0 +1,39 @@ |
||||
package timeutil |
||||
|
||||
import "time" |
||||
|
||||
func FirstSecondInPeriod(since time.Time, period string) (_ time.Time) { |
||||
y, m, d := since.Date() |
||||
|
||||
switch period { |
||||
case "h": |
||||
h := since.Hour() |
||||
return time.Date(y, m, d, h, 0, 0, 0, time.Local) |
||||
case "d": |
||||
return time.Date(y, m, d, 0, 0, 0, 0, time.Local) |
||||
case "m": |
||||
return time.Date(y, m, 1, 0, 0, 0, 0, time.Local) |
||||
default: |
||||
return since |
||||
} |
||||
} |
||||
|
||||
func LastSecondInPeriod(until time.Time, period string) (_ time.Time) { |
||||
y, m, d := until.Date() |
||||
|
||||
switch period { |
||||
case "h": |
||||
h := until.Hour() |
||||
return time.Date(y, m, d, h, 59, 59, 0, time.Local) |
||||
case "d": |
||||
return time.Date(y, m, d, 23, 59, 59, 0, time.Local) |
||||
case "m": |
||||
tm := time.Date(y, m, 1, 23, 59, 59, 0, time.Local) |
||||
// Додаю місяць
|
||||
tm = tm.AddDate(0, 1, 0) |
||||
// Віднімаю день
|
||||
return tm.AddDate(0, 0, -1) |
||||
default: |
||||
return until |
||||
} |
||||
} |
@ -0,0 +1,346 @@ |
||||
package txlog |
||||
|
||||
import ( |
||||
"bufio" |
||||
"bytes" |
||||
"errors" |
||||
"fmt" |
||||
"hash/crc32" |
||||
"io" |
||||
"os" |
||||
|
||||
"gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/proto" |
||||
) |
||||
|
||||
type Reader struct { |
||||
file *os.File |
||||
reader *bufio.Reader |
||||
} |
||||
|
||||
type ReaderOptions struct { |
||||
FileName string |
||||
BufferSize int |
||||
} |
||||
|
||||
func NewReader(opt ReaderOptions) (*Reader, error) { |
||||
if opt.FileName == "" { |
||||
return nil, errors.New("FileName option is required") |
||||
} |
||||
if opt.BufferSize <= 0 { |
||||
return nil, errors.New("BufferSize option is required") |
||||
} |
||||
|
||||
file, err := os.Open(opt.FileName) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return &Reader{ |
||||
file: file, |
||||
reader: bufio.NewReaderSize(file, 1024*1024), |
||||
}, nil |
||||
} |
||||
|
||||
func (s *Reader) Close() { |
||||
s.file.Close() |
||||
} |
||||
|
||||
func (s *Reader) ReadPacket() (uint32, []any, bool, error) { |
||||
prefix := make([]byte, packetPrefixSize) |
||||
n, err := s.reader.Read(prefix) |
||||
if err != nil { |
||||
if err == io.EOF && n == 0 { |
||||
return 0, nil, true, nil |
||||
} else { |
||||
return 0, nil, false, fmt.Errorf("read packet prefix: %s", err) |
||||
} |
||||
} |
||||
|
||||
length := bin.GetUint32(prefix[lengthIdx:]) |
||||
storedCRC := bin.GetUint32(prefix[checksumIdx:]) |
||||
lsn := bin.GetUint32(prefix[lsnIdx:]) |
||||
|
||||
body, err := bin.ReadN(s.reader, int(length)) |
||||
if err != nil { |
||||
return 0, nil, false, fmt.Errorf("read packet body: %s", err) |
||||
} |
||||
|
||||
hasher := crc32.NewIEEE() |
||||
hasher.Write(prefix[lsnIdx:]) |
||||
hasher.Write(body) |
||||
|
||||
calculatedCRC := hasher.Sum32() |
||||
|
||||
if calculatedCRC != storedCRC { |
||||
return 0, nil, false, fmt.Errorf("stored CRC %d != calculated CRC %d", |
||||
storedCRC, calculatedCRC) |
||||
} |
||||
|
||||
records, err := s.parseRecords(body) |
||||
if err != nil { |
||||
return 0, nil, false, err |
||||
} |
||||
return lsn, records, false, nil |
||||
} |
||||
|
||||
func (s *Reader) parseRecords(body []byte) ([]any, error) { |
||||
var ( |
||||
src = bytes.NewBuffer(body) |
||||
records []any |
||||
) |
||||
|
||||
for { |
||||
recordType, err := src.ReadByte() |
||||
if err != nil { |
||||
if err == io.EOF { |
||||
return records, nil |
||||
} |
||||
return nil, err |
||||
} |
||||
|
||||
switch recordType { |
||||
case CodeAddedMetric: |
||||
var rec AddedMetric |
||||
rec, err = s.readAddedMetric(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
records = append(records, rec) |
||||
|
||||
case CodeDeletedMetric: |
||||
var rec DeletedMetric |
||||
rec, err = s.readDeletedMetric(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
records = append(records, rec) |
||||
|
||||
case CodeAppendedMeasure: |
||||
var rec AppendedMeasure |
||||
rec, err = s.readAppendedMeasure(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
records = append(records, rec) |
||||
|
||||
case CodeAppendedMeasures: |
||||
var rec AppendedMeasures |
||||
rec, err = s.readAppendedMeasures(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
records = append(records, rec) |
||||
|
||||
case CodeAppendedMeasureWithOverflow: |
||||
var rec AppendedMeasureWithOverflow |
||||
rec, err = s.readAppendedMeasureWithOverflow(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
records = append(records, rec) |
||||
|
||||
case CodeDeletedMeasures: |
||||
var rec DeletedMeasures |
||||
rec, err = s.readDeletedMeasures(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
records = append(records, rec) |
||||
|
||||
default: |
||||
return nil, fmt.Errorf("unknown record type code: %d", recordType) |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Reader) readAddedMetric(src *bytes.Buffer) (_ AddedMetric, err error) { |
||||
arr, err := bin.ReadN(src, 6) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return AddedMetric{ |
||||
MetricID: bin.GetUint32(arr), |
||||
MetricType: diploma.MetricType(arr[4]), |
||||
FracDigits: int(arr[5]), |
||||
}, nil |
||||
} |
||||
|
||||
func (s *Reader) readDeletedMetric(src *bytes.Buffer) (_ DeletedMetric, err error) { |
||||
var rec DeletedMetric |
||||
rec.MetricID, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
// free data pages
|
||||
dataQty, _, err := bin.ReadVarUint64(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
for range dataQty { |
||||
var pageNo uint32 |
||||
pageNo, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
rec.FreeDataPages = append(rec.FreeDataPages, pageNo) |
||||
} |
||||
// free index pages
|
||||
indexQty, _, err := bin.ReadVarUint64(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
for range indexQty { |
||||
var pageNo uint32 |
||||
pageNo, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
rec.FreeIndexPages = append(rec.FreeIndexPages, pageNo) |
||||
} |
||||
return rec, nil |
||||
} |
||||
|
||||
func (s *Reader) readAppendedMeasure(src *bytes.Buffer) (_ AppendedMeasure, err error) { |
||||
arr, err := bin.ReadN(src, 16) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return AppendedMeasure{ |
||||
MetricID: bin.GetUint32(arr[0:]), |
||||
Timestamp: bin.GetUint32(arr[4:]), |
||||
Value: bin.GetFloat64(arr[8:]), |
||||
}, nil |
||||
} |
||||
|
||||
func (s *Reader) readAppendedMeasures(src *bytes.Buffer) (_ AppendedMeasures, err error) { |
||||
var rec AppendedMeasures |
||||
rec.MetricID, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
qty, err := bin.ReadUint16(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
for range qty { |
||||
var measure proto.Measure |
||||
measure.Timestamp, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
measure.Value, err = bin.ReadFloat64(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
rec.Measures = append(rec.Measures, measure) |
||||
} |
||||
return rec, nil |
||||
} |
||||
|
||||
func (s *Reader) readAppendedMeasureWithOverflow(src *bytes.Buffer) (_ AppendedMeasureWithOverflow, err error) { |
||||
var ( |
||||
b byte |
||||
rec AppendedMeasureWithOverflow |
||||
) |
||||
rec.MetricID, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
rec.Timestamp, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
rec.Value, err = bin.ReadFloat64(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
b, err = src.ReadByte() |
||||
if err != nil { |
||||
return |
||||
} |
||||
rec.IsDataPageReused = b == 1 |
||||
|
||||
rec.DataPageNo, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
b, err = src.ReadByte() |
||||
if err != nil { |
||||
return |
||||
} |
||||
if b == 1 { |
||||
rec.IsRootChanged = true |
||||
rec.RootPageNo, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
} |
||||
// index pages
|
||||
indexQty, err := src.ReadByte() |
||||
if err != nil { |
||||
return |
||||
} |
||||
for range indexQty { |
||||
var pageNo uint32 |
||||
pageNo, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
rec.ReusedIndexPages = append(rec.ReusedIndexPages, pageNo) |
||||
} |
||||
return rec, nil |
||||
} |
||||
|
||||
func (s *Reader) readDeletedMeasures(src *bytes.Buffer) (_ DeletedMeasures, err error) { |
||||
var ( |
||||
rec DeletedMeasures |
||||
) |
||||
rec.MetricID, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
// free data pages
|
||||
rec.FreeDataPages, err = s.readFreePageNumbers(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
// free index pages
|
||||
rec.FreeIndexPages, err = s.readFreePageNumbers(src) |
||||
if err != nil { |
||||
return |
||||
} |
||||
return rec, nil |
||||
} |
||||
|
||||
// HELPERS
|
||||
|
||||
func (s *Reader) readFreePageNumbers(src *bytes.Buffer) ([]uint32, error) { |
||||
var freePages []uint32 |
||||
qty, _, err := bin.ReadVarUint64(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
for range qty { |
||||
var pageNo uint32 |
||||
pageNo, err = bin.ReadUint32(src) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
freePages = append(freePages, pageNo) |
||||
} |
||||
return freePages, nil |
||||
} |
||||
|
||||
func (s *Reader) Seek(offset int64) error { |
||||
ret, err := s.file.Seek(offset, 0) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if ret != offset { |
||||
return fmt.Errorf("ret %d != offset %d", ret, offset) |
||||
} |
||||
return nil |
||||
} |
@ -0,0 +1,507 @@ |
||||
package txlog |
||||
|
||||
import ( |
||||
"bytes" |
||||
"errors" |
||||
"fmt" |
||||
"hash/crc32" |
||||
"os" |
||||
"path/filepath" |
||||
"sync" |
||||
|
||||
octopus "gordenko.dev/dima/diploma" |
||||
"gordenko.dev/dima/diploma/bin" |
||||
"gordenko.dev/dima/diploma/proto" |
||||
) |
||||
|
||||
const ( |
||||
lsnSize = 4 |
||||
packetPrefixSize = 12 // 4 lsn + 4 packet length + 4 crc32
|
||||
|
||||
lengthIdx = 0 |
||||
checksumIdx = 4 |
||||
lsnIdx = 8 |
||||
|
||||
filePerm = 0770 |
||||
|
||||
dumpSnapshotAfterNBytes = 1024 * 1024 * 1024 // 1 GB
|
||||
) |
||||
|
||||
const ( |
||||
CodeAddedMetric byte = 1 |
||||
CodeDeletedMetric byte = 2 |
||||
CodeAppendedMeasure byte = 4 |
||||
CodeAppendedMeasures byte = 5 |
||||
CodeAppendedMeasureWithOverflow byte = 6 |
||||
CodeDeletedMeasures byte = 7 |
||||
) |
||||
|
||||
func JoinChangesFileName(dir string, logNumber int) string { |
||||
return filepath.Join(dir, fmt.Sprintf("%d.changes", logNumber)) |
||||
} |
||||
|
||||
type Changes struct { |
||||
Records []any |
||||
LogNumber int |
||||
ForceSnapshot bool |
||||
ExitWaitGroup *sync.WaitGroup |
||||
WaitCh chan struct{} |
||||
} |
||||
|
||||
type Writer struct { |
||||
mutex sync.Mutex |
||||
logNumber int |
||||
dir string |
||||
file *os.File |
||||
buf *bytes.Buffer |
||||
redoFilesToDelete []string |
||||
workerReqs []any |
||||
waitCh chan struct{} |
||||
appendToWorkerQueue func(any) |
||||
lsn uint32 |
||||
written int64 |
||||
isExited bool |
||||
exitCh chan struct{} |
||||
waitGroup *sync.WaitGroup |
||||
signalCh chan struct{} |
||||
} |
||||
|
||||
type WriterOptions struct { |
||||
Dir string |
||||
LogNumber int // номер журнала
|
||||
AppendToWorkerQueue func(any) |
||||
ExitCh chan struct{} |
||||
WaitGroup *sync.WaitGroup |
||||
} |
||||
|
||||
func NewWriter(opt WriterOptions) (*Writer, error) { |
||||
if opt.Dir == "" { |
||||
return nil, errors.New("Dir option is required") |
||||
} |
||||
if opt.AppendToWorkerQueue == nil { |
||||
return nil, errors.New("AppendToWorkerQueue option is required") |
||||
} |
||||
if opt.ExitCh == nil { |
||||
return nil, errors.New("ExitCh option is required") |
||||
} |
||||
if opt.WaitGroup == nil { |
||||
return nil, errors.New("WaitGroup option is required") |
||||
} |
||||
|
||||
s := &Writer{ |
||||
dir: opt.Dir, |
||||
buf: bytes.NewBuffer(nil), |
||||
appendToWorkerQueue: opt.AppendToWorkerQueue, |
||||
logNumber: opt.LogNumber, |
||||
exitCh: opt.ExitCh, |
||||
waitGroup: opt.WaitGroup, |
||||
signalCh: make(chan struct{}, 1), |
||||
} |
||||
|
||||
var err error |
||||
|
||||
if opt.LogNumber > 0 { |
||||
s.file, err = os.OpenFile( |
||||
JoinChangesFileName(opt.Dir, s.logNumber), |
||||
os.O_APPEND|os.O_WRONLY, |
||||
filePerm, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} else { |
||||
s.logNumber = 1 |
||||
s.file, err = os.OpenFile( |
||||
JoinChangesFileName(opt.Dir, s.logNumber), |
||||
os.O_CREATE|os.O_WRONLY, |
||||
filePerm, |
||||
) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
} |
||||
|
||||
s.reset() |
||||
return s, nil |
||||
} |
||||
|
||||
func (s *Writer) Run() { |
||||
for { |
||||
select { |
||||
case <-s.signalCh: |
||||
if err := s.flush(); err != nil { |
||||
octopus.Abort(octopus.FailedWriteToTxLog, err) |
||||
} |
||||
|
||||
case <-s.exitCh: |
||||
s.exit() |
||||
return |
||||
} |
||||
} |
||||
} |
||||
|
||||
func (s *Writer) reset() { |
||||
s.buf.Reset() |
||||
s.buf.Write([]byte{ |
||||
0, 0, 0, 0, // packet length
|
||||
0, 0, 0, 0, // crc32
|
||||
0, 0, 0, 0, // lsn
|
||||
|
||||
}) |
||||
s.redoFilesToDelete = nil |
||||
s.workerReqs = nil |
||||
s.waitCh = make(chan struct{}) |
||||
} |
||||
|
||||
func (s *Writer) flush() error { |
||||
s.mutex.Lock() |
||||
|
||||
workerReqs := s.workerReqs |
||||
waitCh := s.waitCh |
||||
isExited := s.isExited |
||||
|
||||
var exitWaitGroup *sync.WaitGroup |
||||
if s.isExited { |
||||
exitWaitGroup = s.waitGroup |
||||
} |
||||
|
||||
if s.buf.Len() > packetPrefixSize { |
||||
redoFilesToDelete := s.redoFilesToDelete |
||||
s.lsn++ |
||||
lsn := s.lsn |
||||
packet := make([]byte, s.buf.Len()) |
||||
copy(packet, s.buf.Bytes()) |
||||
s.reset() |
||||
|
||||
s.written += int64(len(packet)) + 12 |
||||
s.mutex.Unlock() |
||||
|
||||
bin.PutUint32(packet[lengthIdx:], uint32(len(packet)-packetPrefixSize)) |
||||
bin.PutUint32(packet[lsnIdx:], lsn) |
||||
bin.PutUint32(packet[checksumIdx:], crc32.ChecksumIEEE(packet[8:])) |
||||
|
||||
n, err := s.file.Write(packet) |
||||
if err != nil { |
||||
return fmt.Errorf("TxLog write: %s", err) |
||||
} |
||||
|
||||
if n != len(packet) { |
||||
return fmt.Errorf("TxLog written %d != packet size %d", n, len(packet)) |
||||
} |
||||
|
||||
if err := s.file.Sync(); err != nil { |
||||
return fmt.Errorf("TxLog sync: %s", err) |
||||
} |
||||
|
||||
for _, fileName := range redoFilesToDelete { |
||||
err = os.Remove(fileName) |
||||
if err != nil { |
||||
octopus.Abort(octopus.RemoveREDOFileFailed, err) |
||||
} |
||||
} |
||||
} else { |
||||
s.waitCh = make(chan struct{}) |
||||
s.mutex.Unlock() |
||||
} |
||||
|
||||
var forceSnapshot bool |
||||
|
||||
if s.written > dumpSnapshotAfterNBytes { |
||||
forceSnapshot = true |
||||
} |
||||
|
||||
if isExited && s.written > 0 { |
||||
forceSnapshot = true |
||||
} |
||||
|
||||
if forceSnapshot { |
||||
if err := s.file.Close(); err != nil { |
||||
return fmt.Errorf("close changes file: %s", err) |
||||
} |
||||
s.logNumber++ |
||||
var err error |
||||
s.file, err = os.OpenFile( |
||||
JoinChangesFileName(s.dir, s.logNumber), |
||||
os.O_CREATE|os.O_WRONLY, |
||||
filePerm, |
||||
) |
||||
if err != nil { |
||||
return fmt.Errorf("create new changes file: %s", err) |
||||
} |
||||
|
||||
s.written = 0 |
||||
} |
||||
|
||||
s.appendToWorkerQueue(Changes{ |
||||
Records: workerReqs, |
||||
ForceSnapshot: forceSnapshot, |
||||
LogNumber: s.logNumber, |
||||
WaitCh: waitCh, |
||||
ExitWaitGroup: exitWaitGroup, |
||||
}) |
||||
return nil |
||||
} |
||||
|
||||
func (s *Writer) exit() { |
||||
s.mutex.Lock() |
||||
s.isExited = true |
||||
s.mutex.Unlock() |
||||
|
||||
if err := s.flush(); err != nil { |
||||
octopus.Abort(octopus.FailedWriteToTxLog, err) |
||||
} |
||||
} |
||||
|
||||
// API
|
||||
|
||||
type AddedMetric struct { |
||||
MetricID uint32 |
||||
MetricType octopus.MetricType |
||||
FracDigits int |
||||
} |
||||
|
||||
func (s *Writer) WriteAddedMetric(req AddedMetric) chan struct{} { |
||||
arr := []byte{ |
||||
CodeAddedMetric, |
||||
0, 0, 0, 0, //
|
||||
byte(req.MetricType), |
||||
byte(req.FracDigits), |
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
// пишу в буфер
|
||||
s.mutex.Lock() |
||||
s.buf.Write(arr) |
||||
s.workerReqs = append(s.workerReqs, req) |
||||
s.mutex.Unlock() |
||||
|
||||
s.sendSignal() |
||||
return s.waitCh |
||||
} |
||||
|
||||
type DeletedMetric struct { |
||||
MetricID uint32 |
||||
FreeDataPages []uint32 |
||||
FreeIndexPages []uint32 |
||||
} |
||||
|
||||
func (s *Writer) WriteDeletedMetric(req DeletedMetric) chan struct{} { |
||||
arr := []byte{ |
||||
CodeDeletedMetric, |
||||
0, 0, 0, 0, // metricID
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
|
||||
// пишу в буфер
|
||||
s.mutex.Lock() |
||||
defer s.mutex.Unlock() |
||||
|
||||
s.buf.Write(arr) |
||||
s.packFreeDataAndIndexPages(req.FreeDataPages, req.FreeIndexPages) |
||||
s.workerReqs = append(s.workerReqs, req) |
||||
|
||||
s.sendSignal() |
||||
return s.waitCh |
||||
} |
||||
|
||||
type AppendedMeasure struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
} |
||||
|
||||
func (s *Writer) WriteAppendMeasure(req AppendedMeasure) chan struct{} { |
||||
arr := []byte{ |
||||
CodeAppendedMeasure, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, 0, 0, // timestamp
|
||||
0, 0, 0, 0, 0, 0, 0, 0, // value
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint32(arr[5:], req.Timestamp) |
||||
bin.PutFloat64(arr[9:], req.Value) |
||||
//
|
||||
s.mutex.Lock() |
||||
s.buf.Write(arr) |
||||
s.workerReqs = append(s.workerReqs, req) |
||||
s.mutex.Unlock() |
||||
|
||||
s.sendSignal() |
||||
return s.waitCh |
||||
} |
||||
|
||||
type AppendedMeasures struct { |
||||
MetricID uint32 |
||||
Measures []proto.Measure |
||||
} |
||||
|
||||
type AppendedMeasuresExtended struct { |
||||
Record AppendedMeasures |
||||
HoldLock bool |
||||
} |
||||
|
||||
func (s *Writer) WriteAppendMeasures(req AppendedMeasures, holdLock bool) chan struct{} { |
||||
arr := []byte{ |
||||
CodeAppendedMeasures, |
||||
0, 0, 0, 0, // metricID
|
||||
0, 0, // qty
|
||||
} |
||||
bin.PutUint32(arr[1:], req.MetricID) |
||||
bin.PutUint16(arr[5:], uint16(len(req.Measures))) |
||||
//
|
||||
s.mutex.Lock() |
||||
s.buf.Write(arr) |
||||
for _, measure := range req.Measures { |
||||
bin.WriteUint32(s.buf, measure.Timestamp) |
||||
bin.WriteFloat64(s.buf, measure.Value) |
||||
} |
||||
s.workerReqs = append(s.workerReqs, AppendedMeasuresExtended{ |
||||
Record: req, |
||||
HoldLock: holdLock, |
||||
}) |
||||
s.mutex.Unlock() |
||||
|
||||
s.sendSignal() |
||||
return s.waitCh |
||||
} |
||||
|
||||
type AppendedMeasureWithOverflow struct { |
||||
MetricID uint32 |
||||
Timestamp uint32 |
||||
Value float64 |
||||
IsDataPageReused bool |
||||
DataPageNo uint32 |
||||
IsRootChanged bool |
||||
RootPageNo uint32 |
||||
ReusedIndexPages []uint32 |
||||
} |
||||
|
||||
type AppendedMeasureWithOverflowExtended struct { |
||||
Record AppendedMeasureWithOverflow |
||||
HoldLock bool |
||||
} |
||||
|
||||
/* |
||||
Формат: |
||||
1b code |
||||
4b metricID |
||||
4b timestamp |
||||
8b value |
||||
1b isReusedDataPage |
||||
4b dataPageNo |
||||
1b isRootChanged |
||||
[4b] newRootPageNo |
||||
1b reusedIndexPages length |
||||
[N * 4b] reusedIndexPages |
||||
*/ |
||||
func (s *Writer) WriteAppendedMeasureWithOverflow(req AppendedMeasureWithOverflow, redoFileName string, holdLock bool) chan struct{} { |
||||
size := 24 + len(req.ReusedIndexPages)*4 |
||||
if req.IsRootChanged { |
||||
size += 4 |
||||
} |
||||
|
||||
tmp := make([]byte, size) |
||||
|
||||
tmp[0] = CodeAppendedMeasureWithOverflow |
||||
bin.PutUint32(tmp[1:], req.MetricID) |
||||
bin.PutUint32(tmp[5:], req.Timestamp) |
||||
bin.PutFloat64(tmp[9:], req.Value) |
||||
if req.IsDataPageReused { |
||||
tmp[17] = 1 |
||||
} |
||||
bin.PutUint32(tmp[18:], req.DataPageNo) |
||||
|
||||
pos := 22 |
||||
if req.IsRootChanged { |
||||
tmp[pos] = 1 |
||||
bin.PutUint32(tmp[pos+1:], req.RootPageNo) |
||||
pos += 5 |
||||
} else { |
||||
tmp[pos] = 0 |
||||
pos += 1 |
||||
} |
||||
|
||||
tmp[pos] = byte(len(req.ReusedIndexPages)) |
||||
pos += 1 |
||||
for _, indexPageNo := range req.ReusedIndexPages { |
||||
bin.PutUint32(tmp[pos:], indexPageNo) |
||||
pos += 4 |
||||
} |
||||
|
||||
s.mutex.Lock() |
||||
s.buf.Write(tmp) |
||||
s.workerReqs = append(s.workerReqs, AppendedMeasureWithOverflowExtended{ |
||||
Record: req, |
||||
HoldLock: holdLock, |
||||
}) |
||||
s.redoFilesToDelete = append(s.redoFilesToDelete, redoFileName) |
||||
s.mutex.Unlock() |
||||
|
||||
s.sendSignal() |
||||
return s.waitCh |
||||
} |
||||
|
||||
type DeletedMeasures struct { |
||||
MetricID uint32 |
||||
FreeDataPages []uint32 |
||||
FreeIndexPages []uint32 |
||||
} |
||||
|
||||
/* |
||||
Формат: |
||||
1b code |
||||
4b metricID |
||||
1b freeDataPages length |
||||
[N * 4b] freeDataPages |
||||
1b freeIndexPages length |
||||
[N * 4b] freeIndexPages |
||||
*/ |
||||
func (s *Writer) WriteDeletedMeasures(op DeletedMeasures) chan struct{} { |
||||
tmp := []byte{ |
||||
CodeDeletedMeasures, |
||||
0, 0, 0, 0, |
||||
} |
||||
bin.PutUint32(tmp[1:], op.MetricID) |
||||
|
||||
// записываю часть фиксированного размера
|
||||
s.mutex.Lock() |
||||
s.buf.Write(tmp) |
||||
s.packFreeDataAndIndexPages(op.FreeDataPages, op.FreeIndexPages) |
||||
s.workerReqs = append(s.workerReqs, op) |
||||
s.mutex.Unlock() |
||||
|
||||
s.sendSignal() |
||||
return s.waitCh |
||||
} |
||||
|
||||
type DeletedMeasuresSince struct { |
||||
MetricID uint32 |
||||
LastPageNo uint32 |
||||
IsRootChanged bool |
||||
RootPageNo uint32 |
||||
FreeDataPages []uint32 |
||||
FreeIndexPages []uint32 |
||||
TimestampsBuf []byte |
||||
ValuesBuf []byte |
||||
} |
||||
|
||||
func (s *Writer) sendSignal() { |
||||
select { |
||||
case s.signalCh <- struct{}{}: |
||||
default: |
||||
} |
||||
} |
||||
|
||||
// helper
|
||||
|
||||
func (s *Writer) packFreeDataAndIndexPages(freeDataPages, freeIndexPages []uint32) { |
||||
// записываю data pages
|
||||
bin.WriteVarUint64(s.buf, uint64(len(freeDataPages))) |
||||
for _, dataPageNo := range freeDataPages { |
||||
bin.WriteUint32(s.buf, dataPageNo) |
||||
} |
||||
// записываю index pages
|
||||
bin.WriteVarUint64(s.buf, uint64(len(freeIndexPages))) |
||||
for _, indexPageNo := range freeIndexPages { |
||||
bin.WriteUint32(s.buf, indexPageNo) |
||||
} |
||||
} |
@ -0,0 +1,11 @@ |
||||
cd examples/database |
||||
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ../../database_windows |
||||
cd - |
||||
|
||||
cd examples/loadtest |
||||
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ../../loadtest_windows |
||||
cd - |
||||
|
||||
cd examples/requests |
||||
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ../../requests_windows |
||||
cd - |
Loading…
Reference in new issue