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)) }