diff --git a/.golangci.yaml b/.golangci.yaml index 0e8c58e..0f14449 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -213,8 +213,7 @@ linters-settings: gocognit: # Minimal code complexity to report. - # Default: 30 (but we recommend 10-20) - min-complexity: 20 + min-complexity: 30 gocritic: # Settings passed to gocritic. diff --git a/clock/clock.go b/clock/clock.go new file mode 100644 index 0000000..76466ca --- /dev/null +++ b/clock/clock.go @@ -0,0 +1,13 @@ +package clock + +import ( + "time" +) + +// Clock is a time abstraction which can be used to if you need to mock time in tests. +type Clock interface { + Now() time.Time + NewTicker(d time.Duration) (<-chan time.Time, func()) + NewTimer(d time.Duration) (<-chan time.Time, func() bool) + Since(t time.Time) time.Duration +} diff --git a/server/clock.go b/clock/mock.go similarity index 54% rename from server/clock.go rename to clock/mock.go index 5bb603f..1532313 100644 --- a/server/clock.go +++ b/clock/mock.go @@ -1,4 +1,4 @@ -package server +package clock import ( "sync" @@ -6,62 +6,37 @@ import ( "time" ) -// Clock is a simple abstraction to allow for time based assertions in tests. -type Clock interface { - Now() time.Time - NewTicker(d time.Duration) (<-chan time.Time, func()) - NewTimer(d time.Duration) (<-chan time.Time, func() bool) -} - -type RealClock struct{} - -func NewClock() *RealClock { - return &RealClock{} -} - -func (c *RealClock) Now() time.Time { - return time.Now() -} - -func (c *RealClock) NewTicker(d time.Duration) (<-chan time.Time, func()) { - t := time.NewTicker(d) - return t.C, t.Stop -} - -func (c *RealClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) { - t := time.NewTimer(d) - return t.C, t.Stop -} - -type testTimer struct { +type mockTimer struct { deadline time.Time ch chan time.Time stopped *atomic.Bool } -type testTicker struct { +type mockTicker struct { nextTick time.Time interval time.Duration ch chan time.Time stopped *atomic.Bool } -type TestClock struct { +// MockClock is a mock implementation of the Clock interface. +type MockClock struct { mu sync.Mutex time time.Time - timers []*testTimer - tickers []*testTicker + timers []*mockTimer + tickers []*mockTicker } -func NewTestClock(time time.Time) *TestClock { - var c TestClock +func NewMock(time time.Time) *MockClock { + var c MockClock c.time = time - c.timers = make([]*testTimer, 0) - c.tickers = make([]*testTicker, 0) + c.timers = make([]*mockTimer, 0) + c.tickers = make([]*mockTicker, 0) return &c } -func (c *TestClock) Set(t time.Time) { +// Set sets the time of the clock and triggers any timers or tickers that should fire. +func (c *MockClock) Set(t time.Time) { c.mu.Lock() defer c.mu.Unlock() @@ -72,7 +47,7 @@ func (c *TestClock) Set(t time.Time) { c.time = t for _, ticker := range c.tickers { if !ticker.stopped.Load() && !ticker.nextTick.Add(ticker.interval).After(c.time) { - //nolint: durationcheck // This is a test clock where we can ignore overflows. + //nolint: durationcheck // This is a test clock, we don't care about overflows. nextTick := (c.time.Sub(ticker.nextTick) / ticker.interval) * ticker.interval ticker.nextTick = ticker.nextTick.Add(nextTick) select { @@ -82,7 +57,7 @@ func (c *TestClock) Set(t time.Time) { } } - unfiredTimers := make([]*testTimer, 0) + unfiredTimers := make([]*mockTimer, 0) for i, timer := range c.timers { if timer.deadline.After(c.time) && !timer.stopped.Load() { unfiredTimers = append(unfiredTimers, c.timers[i]) @@ -94,23 +69,26 @@ func (c *TestClock) Set(t time.Time) { c.timers = unfiredTimers } -func (c *TestClock) Add(d time.Duration) { +// Add advances the clock by the duration and triggers any timers or tickers that should fire. +func (c *MockClock) Add(d time.Duration) { c.Set(c.time.Add(d)) } -func (c *TestClock) Now() time.Time { +// Now returns the current time of the clock. +func (c *MockClock) Now() time.Time { c.mu.Lock() defer c.mu.Unlock() return c.time } -func (c *TestClock) NewTicker(d time.Duration) (<-chan time.Time, func()) { +// NewTicker creates a new ticker that will fire once you advance the clock by the duration. +func (c *MockClock) NewTicker(d time.Duration) (<-chan time.Time, func()) { c.mu.Lock() defer c.mu.Unlock() ch := make(chan time.Time, 1) stopped := &atomic.Bool{} - ticker := &testTicker{nextTick: c.time, interval: d, ch: ch, stopped: stopped} + ticker := &mockTicker{nextTick: c.time, interval: d, ch: ch, stopped: stopped} c.tickers = append(c.tickers, ticker) stop := func() { stopped.Store(true) @@ -119,7 +97,8 @@ func (c *TestClock) NewTicker(d time.Duration) (<-chan time.Time, func()) { return ch, stop } -func (c *TestClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) { +// NewTimer creates a new timer that will fire once you advance the clock by the duration. +func (c *MockClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) { c.mu.Lock() defer c.mu.Unlock() @@ -132,7 +111,7 @@ func (c *TestClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) { return ch, func() bool { return false } } - timer := &testTimer{deadline: c.time.Add(d), ch: ch, stopped: stopped} + timer := &mockTimer{deadline: c.time.Add(d), ch: ch, stopped: stopped} c.timers = append(c.timers, timer) stop := func() bool { return stopped.CompareAndSwap(false, true) @@ -140,3 +119,8 @@ func (c *TestClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) { return ch, stop } + +// Since calculates the time since the given time t. +func (c *MockClock) Since(t time.Time) time.Duration { + return c.Now().Sub(t) +} diff --git a/clock/real.go b/clock/real.go new file mode 100644 index 0000000..5850dd6 --- /dev/null +++ b/clock/real.go @@ -0,0 +1,32 @@ +package clock + +import "time" + +// RealClock imlements the Clock interface using the standard libraries time package. +type RealClock struct{} + +func New() *RealClock { + return &RealClock{} +} + +// Now is a wrapper around time.Now(). +func (c *RealClock) Now() time.Time { + return time.Now() +} + +// NewTicker is a wrapper around time.NewTicker(). +func (c *RealClock) NewTicker(d time.Duration) (<-chan time.Time, func()) { + t := time.NewTicker(d) + return t.C, t.Stop +} + +// NewTimer is a wrapper around time.NewTimer(). +func (c *RealClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) { + t := time.NewTimer(d) + return t.C, t.Stop +} + +// Since is a wrapper around time.Since(). +func (c *RealClock) Since(t time.Time) time.Duration { + return time.Since(t) +} diff --git a/logdb.go b/logdb.go index 100dfa8..4cab09d 100644 --- a/logdb.go +++ b/logdb.go @@ -6,6 +6,7 @@ import ( "time" "github.com/charmbracelet/log" + "github.com/creativecreature/pulse/clock" ) const ( @@ -23,13 +24,14 @@ type Record struct { type LogDB struct { sync.RWMutex dirPath string + clock clock.Clock log *log.Logger head *Segment tail *Segment } // NewDB creates a new log database. -func NewDB(dirPath string) *LogDB { +func NewDB(dirPath string, clock clock.Clock) *LogDB { log := NewLogger() // Create the directory if it doesn't exist. @@ -45,13 +47,15 @@ func NewDB(dirPath string) *LogDB { var logDB LogDB logDB.dirPath = dirPath logDB.log = log + logDB.clock = clock // Leak a goroutine that compacts the segments. defer func() { go func() { - ticker := time.NewTicker(segmentationInterval) + c, cancel := clock.NewTicker(segmentationInterval) + defer cancel() for { - <-ticker.C + <-c logDB.compact() } }() @@ -60,7 +64,7 @@ func NewDB(dirPath string) *LogDB { // If the directory is empty, we'll simply create the initial segment and return. if len(segmentPaths) == 0 { segment := newSegment(dirPath, 0) - logDB.head, logDB.tail = segment, segment + logDB.head, logDB.tail = segment, nil return &logDB } @@ -79,6 +83,8 @@ func NewDB(dirPath string) *LogDB { // appendSegment creates a new segment and appends it to the // head of the linked list. should be called with a lock. func (db *LogDB) appendSegment() { + db.log.Info("Appending a new segment") + nextSegmentIndex := db.head.index + 1 segment := newSegment(db.dirPath, nextSegmentIndex) @@ -96,58 +102,84 @@ func (db *LogDB) appendSegment() { // compact compacts all of the segments together, removing any duplicate keys. func (db *LogDB) compact() { - db.log.Info("Compacting segments") + db.Lock() + defer db.Unlock() - head, current := db.head, db.tail + currentHead, currentTail := db.head, db.tail + current := currentTail if current == nil { - db.log.Info("No segments to compact") + db.log.Info("Not enough segments to necessitate a compaction") return } - for { + db.log.Info("Compacting segments") + valuesToWrite := make(map[string][]byte) + + for current != nil { + current.Lock() for key := range current.hashIndex { var found bool - - for cursor := head; cursor != current; cursor = cursor.next { - _, found = cursor.get(key) + for cursor := currentHead; cursor != current; cursor = cursor.next { + _, found = cursor.getNoLock(key) if found { - current.Lock() - delete(current.hashIndex, key) - current.Unlock() break } } // If this key was unique for all previous segments, we'll write it to the head. if !found { - bytes, _ := current.get(key) - db.MustSet(key, bytes) + bytes, _ := current.getNoLock(key) + valuesToWrite[key] = bytes } } - // Delete the segment file once we've compacted it. - current.Lock() - current.delete() - current.Unlock() - - // Exit the loop if we've reached the head. - if current.prev == head { + // Adjust pointers to remove the current segment + if current == currentHead { + // Only one segment left, no need to delete it + current.Unlock() break } - db.Lock() - current = current.prev - current.next = db.head - db.head.prev = current - db.tail = current - db.Unlock() + prev := current.prev + next := current.next + + if prev != nil { + prev.next = next + } + if next != nil { + next.prev = prev + } + + if current == db.tail { + db.tail = prev + } + if current == db.head { + db.head = next + } + + if err := current.delete(); err != nil { + db.log.Error(err) + current.Unlock() + return + } + + nextSegment := prev + current.Unlock() + current = nextSegment + } + + if db.head == db.tail { + db.tail = nil } - log.Info("Finished compacting segments") + + for key, value := range valuesToWrite { + db.mustSet(key, value) + } + db.log.Info("Finished compacting segments") } // Get retrieves a value from the database. func (db *LogDB) Get(key string) ([]byte, bool) { - db.log.Debug("Getting key", key) db.RLock() defer db.RUnlock() @@ -166,9 +198,32 @@ func (db *LogDB) Get(key string) ([]byte, bool) { return nil, false } +func (db *LogDB) GetAllUnique() map[string][]byte { + db.Lock() + defer db.Unlock() + + values := make(map[string][]byte, len(db.head.hashIndex)) + current := db.head + for { + for key := range current.hashIndex { + if _, ok := values[key]; !ok { + value, _ := current.get(key) + values[key] = value + } + } + + // Update current and break if we've reached the tail. + if current.next == db.head || current.next == nil { + break + } + + current = current.next + } + return values +} + // Set writes a key-value pair to the log file. func (db *LogDB) Set(key string, value []byte) error { - db.log.Debug("writing key", key) db.Lock() defer db.Unlock() @@ -182,10 +237,32 @@ func (db *LogDB) Set(key string, value []byte) error { return nil } +// Set writes a key-value pair without locking the database. +func (db *LogDB) set(key string, value []byte) error { + err := db.head.set(key, value) + if err != nil { + return err + } + if db.head.size() >= segmentSizeBytes { + db.appendSegment() + } + return nil +} + // MustSet writes a key-value pair to the log file and panics on error. func (db *LogDB) MustSet(key string, value []byte) { err := db.Set(key, value) if err != nil { + db.log.Error("%v", err) + panic(err) + } +} + +// mustSet writes a key-value pair without a lock and panics on error. +func (db *LogDB) mustSet(key string, value []byte) { + err := db.set(key, value) + if err != nil { + db.log.Error("%v", err) panic(err) } } @@ -193,44 +270,49 @@ func (db *LogDB) MustSet(key string, value []byte) { // Aggregate gathers all the unique key-value pairs in the database, // and then removes all the segments and resets the state. func (db *LogDB) Aggregate() map[string][]byte { - db.log.Debug("Aggregating segments") + db.log.Info("Aggregating segments") db.Lock() defer db.Unlock() values := make(map[string][]byte, len(db.head.hashIndex)) - - // Break the connection between the tail and head. - if db.tail != nil { - db.tail.next = nil - } - - for current := db.head; current != nil; { + current := db.head + for { for key := range current.hashIndex { if _, ok := values[key]; !ok { value, _ := current.get(key) values[key] = value } - delete(current.hashIndex, key) } + // Check if we've reached the end of the linked list. current.Lock() - current.delete() - current.Unlock() - - // Update current and break if we've reached the tail. - current = current.next - if current == nil { + if current.next == db.head || current.next == nil { + err := current.delete() + if err != nil { + db.log.Error(err) + } + current.next = nil + current.Unlock() break } - // Update the references so it can be GC'd. + err := current.delete() + if err != nil { + db.log.Error(err) + } + current.prev.next = nil current.prev = nil + current.Unlock() + current = current.next } + db.head = nil + db.tail = nil + segment := newSegment(db.dirPath, 0) - db.head, db.tail = segment, segment + db.head, db.tail = segment, nil - db.log.Debug("Finished the aggrementation process") + db.log.Info("Aggregation completed") return values } diff --git a/logdb_test.go b/logdb_test.go index 1578906..83a6612 100644 --- a/logdb_test.go +++ b/logdb_test.go @@ -1,18 +1,75 @@ package pulse_test import ( + "io" + "os" + "path/filepath" "runtime" + "strconv" "sync" "testing" + "time" "github.com/creativecreature/pulse" + "github.com/creativecreature/pulse/clock" ) +func copyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + if err != nil { + return err + } + + err = destFile.Sync() + if err != nil { + return err + } + + return nil +} + +func copyDir(srcDir, dstDir string) error { + return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + return err + } + destPath := filepath.Join(dstDir, relPath) + + err = copyFile(path, destPath) + if err != nil { + return err + } + + return nil + }) +} + func TestConcurrentGetSet(t *testing.T) { cpus := runtime.NumCPU() writeCPUs, readCPUs := cpus/2, cpus/2 numIterations := 10_000 - db := pulse.NewDB(t.TempDir()) + db := pulse.NewDB(t.TempDir(), clock.New()) wg := sync.WaitGroup{} wg.Add(numIterations * (writeCPUs + readCPUs)) @@ -38,10 +95,236 @@ func TestConcurrentGetSet(t *testing.T) { wg.Wait() } -// func TestAggregation(t *testing.T) { -// db := pulse.NewDB("testdata/segments") -// values := db.Aggregate() -// if len(values) != 0 { -// t.Fatalf("expected 0 values, got %d", len(values)) -// } -// } +func TestUniqueValues(t *testing.T) { + t.Parallel() + + path := t.TempDir() + err := copyDir("testdata/segments/two", path) + if err != nil { + t.Fatal(err) + } + + mockClock := clock.NewMock(time.Now()) + db := pulse.NewDB(path, mockClock) + + values := db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } +} + +func TestAggregation(t *testing.T) { + t.Parallel() + + path := t.TempDir() + err := copyDir("testdata/segments/two", path) + if err != nil { + t.Fatal(err) + } + + mockClock := clock.NewMock(time.Now()) + db := pulse.NewDB(path, mockClock) + + values := db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } + + aggregatedValues := db.Aggregate() + if len(aggregatedValues) != 11 { + t.Errorf("expected 11 values, got %d", len(aggregatedValues)) + } +} + +func TestCompaction(t *testing.T) { + t.Parallel() + + path := t.TempDir() + err := copyDir("testdata/segments/two", path) + if err != nil { + t.Fatal(err) + } + + mockClock := clock.NewMock(time.Now()) + db := pulse.NewDB(path, mockClock) + + values := db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } + + mockClock.Add(time.Minute * 4) + time.Sleep(time.Millisecond * 250) + + values = db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } +} + +func TestAggregationAfterCompaction(t *testing.T) { + t.Parallel() + + path := t.TempDir() + err := copyDir("testdata/segments/three", path) + if err != nil { + t.Fatal(err) + } + + mockClock := clock.NewMock(time.Now()) + db := pulse.NewDB(path, mockClock) + + values := db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } + + mockClock.Add(time.Minute * 4) + time.Sleep(time.Millisecond * 250) + + values = db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } + + aggregatedValues := db.Aggregate() + if len(aggregatedValues) != 11 { + t.Errorf("expected 11 values, got %d", len(aggregatedValues)) + } +} + +func TestCompactionWritesAggregation(t *testing.T) { + t.Parallel() + + path := t.TempDir() + err := copyDir("testdata/segments/two", path) + if err != nil { + t.Fatal(err) + } + + mockClock := clock.NewMock(time.Now()) + db := pulse.NewDB(path, mockClock) + + values := db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } + + mockClock.Add(time.Minute * 4) + time.Sleep(time.Millisecond * 250) + + values = db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } + + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + db.MustSet("key"+strconv.Itoa(i), []byte("value")) + } + } + + values = db.GetAllUnique() + if len(values) != 21 { + t.Errorf("expected 21 values, got %d", len(values)) + } + + aggregatedValues := db.Aggregate() + if len(aggregatedValues) != 21 { + t.Errorf("expected 21 values, got %d", len(aggregatedValues)) + } + values = db.GetAllUnique() + if len(values) != 0 { + t.Errorf("expected 0 values, got %d", len(values)) + } + + for i := 0; i < 10; i++ { + for j := 0; j < 10; j++ { + db.MustSet("key"+strconv.Itoa(i), []byte("value")) + } + } + values = db.GetAllUnique() + if len(values) != 10 { + t.Errorf("expected 10 values, got %d", len(values)) + } +} + +func TestAppendingCompactingWritesAggregation(t *testing.T) { + t.Parallel() + + path := t.TempDir() + err := copyDir("testdata/segments/two", path) + if err != nil { + t.Fatal(err) + } + + mockClock := clock.NewMock(time.Now()) + db := pulse.NewDB(path, mockClock) + + values := db.GetAllUnique() + if len(values) != 11 { + t.Errorf("expected 11 values, got %d", len(values)) + } + + for i := 0; i < 1000; i++ { + for j := 0; j < 10; j++ { + db.MustSet("key"+strconv.Itoa(i), []byte("value")) + } + } + + values = db.GetAllUnique() + if len(values) != 1011 { + t.Errorf("expected 1011 values, got %d", len(values)) + } + + mockClock.Add(time.Minute * 4) + time.Sleep(time.Millisecond * 500) + + values = db.GetAllUnique() + if len(values) != 1011 { + t.Errorf("expected 1011 values, got %d", len(values)) + } + + aggregatedValues := db.Aggregate() + if len(aggregatedValues) != 1011 { + t.Errorf("expected 1011 values, got %d", len(aggregatedValues)) + } + values = db.GetAllUnique() + if len(values) != 0 { + t.Errorf("expected 0 values, got %d", len(values)) + } + + for i := 0; i < 1000; i++ { + for j := 0; j < 10; j++ { + db.MustSet("key"+strconv.Itoa(i), []byte("value")) + } + } + + values = db.GetAllUnique() + if len(values) != 1000 { + t.Errorf("expected 1000 values, got %d", len(values)) + } +} + +func TestWritesCompacting(t *testing.T) { + t.Parallel() + + mockClock := clock.NewMock(time.Now()) + db := pulse.NewDB(t.TempDir(), mockClock) + + values := db.GetAllUnique() + if len(values) != 0 { + t.Errorf("expected 0 values, got %d", len(values)) + } + + for i := 0; i < 1000; i++ { + for j := 0; j < 10; j++ { + db.MustSet("key"+strconv.Itoa(i), []byte("value")) + } + } + + values = db.GetAllUnique() + if len(values) != 1000 { + t.Errorf("expected 1000 values, got %d", len(values)) + } +} diff --git a/restore.go b/restore.go index 39bd58f..4c2dae5 100644 --- a/restore.go +++ b/restore.go @@ -56,7 +56,7 @@ func restoreSegment(path string) (*Segment, error) { // connectSegments links all segments together in a circular doubly linked list. func connectSegments(segments []*Segment) { for i := 0; i < len(segments); i++ { - if i == 0 { + if i == 0 && len(segments) > 1 { segments[i].prev = segments[len(segments)-1] segments[i].next = segments[i+1] continue diff --git a/segment.go b/segment.go index c93fa76..2fd67ea 100644 --- a/segment.go +++ b/segment.go @@ -63,6 +63,25 @@ func (s *Segment) get(key string) ([]byte, bool) { return record.Value, true } +func (s *Segment) getNoLock(key string) ([]byte, bool) { + offset, ok := s.hashIndex[key] + if !ok { + return nil, false + } + _, err := s.logFile.Seek(offset, io.SeekStart) + if err != nil { + return nil, false + } + + var record Record + decoder := json.NewDecoder(s.logFile) + if decodeErr := decoder.Decode(&record); decodeErr != nil { + return nil, false + } + + return record.Value, true +} + // set writes a key-value pair to the segments log file. func (s *Segment) set(key string, value []byte) error { s.Lock() @@ -99,10 +118,7 @@ func (s *Segment) size() int64 { // delete closes the file descriptor and removes the segment file from disk. // should be called with a lock. -func (s *Segment) delete() { +func (s *Segment) delete() error { s.logFile.Close() - err := os.Remove(s.logFile.Name()) - if err != nil { - panic(err) - } + return os.Remove(s.logFile.Name()) } diff --git a/server/aggregate.go b/server/aggregate.go index 498bf71..ff16598 100644 --- a/server/aggregate.go +++ b/server/aggregate.go @@ -8,7 +8,7 @@ import ( "github.com/creativecreature/pulse" ) -const aggregationInterval = 15 * time.Minute +const aggregationInterval = 10 * time.Minute // writeToRemote will write the session to the remote storage. func (s *Server) writeToRemote(session pulse.CodingSession) { @@ -23,8 +23,8 @@ func (s *Server) writeToRemote(session pulse.CodingSession) { } func (s *Server) aggregate() { - s.mutex.Lock() - defer s.mutex.Unlock() + s.mu.Lock() + defer s.mu.Unlock() buffers := make(pulse.Buffers, 0) values := s.db.Aggregate() diff --git a/server/handlers.go b/server/handlers.go index 4fb9dd7..d917f91 100644 --- a/server/handlers.go +++ b/server/handlers.go @@ -6,11 +6,11 @@ import ( // FocusGained is invoked by the FocusGained autocommand. func (s *Server) FocusGained(event pulse.Event, reply *string) { - s.mutex.Lock() - defer s.mutex.Unlock() + s.mu.Lock() + defer s.mu.Unlock() s.lastHeartbeat = s.clock.Now() - s.log.Debug("Received FocusGained event.", + s.log.Debug("Received FocusGained event", "editor_id", event.EditorID, "editor", event.Editor, "os", event.OS, @@ -21,16 +21,16 @@ func (s *Server) FocusGained(event pulse.Event, reply *string) { } s.openFile(event) - *reply = "Successfully updated the client being focused." + *reply = "Successfully updated the client being focused" } // OpenFile gets invoked by the *BufEnter* autocommand. func (s *Server) OpenFile(event pulse.Event, reply *string) { - s.mutex.Lock() - defer s.mutex.Unlock() + s.mu.Lock() + defer s.mu.Unlock() s.lastHeartbeat = s.clock.Now() - s.log.Debug("Received OpenFile event.", + s.log.Debug("Received OpenFile event", "editor_id", event.EditorID, "editor", event.Editor, "os", event.OS, @@ -41,18 +41,18 @@ func (s *Server) OpenFile(event pulse.Event, reply *string) { } s.openFile(event) - *reply = "Successfully updated the current file." + *reply = "Successfully updated the current file" } // SendHeartbeat can be called for events such as buffer writes and cursor moves. // Its purpose is to notify the server that the current session remains active. // The server ends the session if it doesn't receive a heartbeat for 10 minutes. func (s *Server) SendHeartbeat(event pulse.Event, reply *string) { - s.mutex.Lock() - defer s.mutex.Unlock() + s.mu.Lock() + defer s.mu.Unlock() s.lastHeartbeat = s.clock.Now() - s.log.Debug("Received heartbeat.", + s.log.Debug("Received heartbeat", "editor_id", event.EditorID, "editor", event.Editor, "os", event.OS, @@ -62,8 +62,8 @@ func (s *Server) SendHeartbeat(event pulse.Event, reply *string) { // EndSession should be called by the *VimLeave* autocommand. func (s *Server) EndSession(event pulse.Event, reply *string) { - s.mutex.Lock() - defer s.mutex.Unlock() + s.mu.Lock() + defer s.mu.Unlock() s.log.Debug("Received EndSession event", "editor_id", event.EditorID, @@ -71,5 +71,5 @@ func (s *Server) EndSession(event pulse.Event, reply *string) { "os", event.OS, ) s.saveBuffer() - *reply = "The session was ended successfully." + *reply = "The session was ended successfully" } diff --git a/server/heartbeat.go b/server/heartbeat.go index 4d1a186..ded4e4e 100644 --- a/server/heartbeat.go +++ b/server/heartbeat.go @@ -13,10 +13,10 @@ const ( // CheckHeartbeat is used to check if the session has been inactive for more than // ten minutes. If that is the case, the session will be terminated and saved to disk. func (s *Server) checkHeartbeat() { - s.mutex.Lock() - defer s.mutex.Unlock() + s.mu.Lock() + defer s.mu.Unlock() - s.log.Debug("Checking heartbeat.", + s.log.Debug("Checking heartbeat", "last_heartbeat", s.lastHeartbeat, "time_now", s.clock.Now().UnixMilli(), ) @@ -27,7 +27,7 @@ func (s *Server) checkHeartbeat() { if s.clock.Now().After(s.lastHeartbeat.Add(HeartbeatTTL)) { s.log.Info( - "Writing the current buffer to disk due to inactivity.", + "Writing the current buffer to disk due to inactivity", "last_heartbeat", strconv.FormatInt(s.lastHeartbeat.UnixMilli(), 10), "current_time", strconv.FormatInt(s.clock.Now().UnixMilli(), 10), "end_time", strconv.FormatInt(s.lastHeartbeat.Add(HeartbeatTTL).UnixMilli(), 10), diff --git a/server/options.go b/server/options.go index 2827f0a..28758ea 100644 --- a/server/options.go +++ b/server/options.go @@ -5,12 +5,13 @@ import ( "github.com/charmbracelet/log" "github.com/creativecreature/pulse" + "github.com/creativecreature/pulse/clock" ) type Option func(*Server) error // WithClock sets the clock used by the server. -func WithClock(clock Clock) Option { +func WithClock(clock clock.Clock) Option { return func(a *Server) error { if clock == nil { return errors.New("clock is nil") diff --git a/server/server.go b/server/server.go index 2876933..0d6656f 100644 --- a/server/server.go +++ b/server/server.go @@ -15,6 +15,7 @@ import ( "github.com/charmbracelet/log" "github.com/creativecreature/pulse" + "github.com/creativecreature/pulse/clock" "github.com/creativecreature/pulse/git" ) @@ -23,27 +24,27 @@ type CodingSessionWriter interface { } type Server struct { - name string + mu sync.Mutex + clock clock.Clock + log *log.Logger activeBuffer *pulse.Buffer + name string lastHeartbeat time.Time - stopJobs chan struct{} - clock Clock - log *log.Logger - mutex sync.Mutex - db *pulse.LogDB sessionWriter CodingSessionWriter + db *pulse.LogDB + stopJobs chan struct{} } // New creates a new server. func New(serverName, segmentPath string, sessionWriter CodingSessionWriter, opts ...Option) (*Server, error) { s := &Server{ - name: serverName, - clock: NewClock(), - stopJobs: make(chan struct{}), - db: pulse.NewDB(segmentPath), + clock: clock.New(), log: pulse.NewLogger(), + name: serverName, sessionWriter: sessionWriter, + stopJobs: make(chan struct{}), } + for _, opt := range opts { err := opt(s) if err != nil { @@ -51,6 +52,8 @@ func New(serverName, segmentPath string, sessionWriter CodingSessionWriter, opts } } + s.db = pulse.NewDB(segmentPath, s.clock) + // Run the heartbeat checks and aggregations in the background. s.runHeartbeatChecks() s.runAggregations() @@ -66,7 +69,7 @@ func (s *Server) openFile(event pulse.Event) { if s.activeBuffer != nil { if s.activeBuffer.Filepath == gitFile.Path && s.activeBuffer.Repository == gitFile.Repository { - s.log.Debug("This buffer is already considered active.", + s.log.Debug("This buffer is already considered active", "path", gitFile.Path, "repository", gitFile.Repository, "editor_id", event.EditorID, @@ -94,13 +97,14 @@ func (s *Server) saveBuffer() { return } - s.log.Debug("Writing the buffer.") + s.log.Debug("Writing the buffer") buf := s.activeBuffer buf.Close(s.clock.Now()) key := buf.Key() // Merge the duration with the most recent entry for this day. if bytes, hasMostRecentEntry := s.db.Get(key); hasMostRecentEntry { + s.log.Debug("Merging with the most recent entry for this buffer") var b pulse.Buffer err := json.Unmarshal(bytes, &b) if err != nil { @@ -167,7 +171,7 @@ func (s *Server) Start(port string) error { } s.saveBuffer() - s.log.Info("Shutting down.") + s.log.Info("Shutting down") return nil } diff --git a/server/server_test.go b/server/server_test.go index a974f99..a37d474 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -12,6 +12,7 @@ import ( "github.com/charmbracelet/log" "github.com/creativecreature/pulse" + "github.com/creativecreature/pulse/clock" "github.com/creativecreature/pulse/server" ) @@ -61,7 +62,7 @@ func TestServerMergesFiles(t *testing.T) { } }() - mockClock := server.NewTestClock(time.Now()) + mockClock := clock.NewMock(time.Now()) mockStorage := newMockStorage() reply := "" diff --git a/testdata/segments/aaaaaaaaaaaaaaaa.log b/testdata/segments/one/aaaaaaaaaaaaaaaa.log similarity index 100% rename from testdata/segments/aaaaaaaaaaaaaaaa.log rename to testdata/segments/one/aaaaaaaaaaaaaaaa.log diff --git a/testdata/segments/three/aaaaaaaaaaaaaaaa.log b/testdata/segments/three/aaaaaaaaaaaaaaaa.log new file mode 100644 index 0000000..8c38a06 --- /dev/null +++ b/testdata/segments/three/aaaaaaaaaaaaaaaa.log @@ -0,0 +1,49 @@ +{"key":"2024-06-16_pulse_pulse/logdb.go","value":"eyJkdXJhdGlvbiI6NzMwNDU5MjgwMDAsImZpbGVuYW1lIjoibG9nZGIuZ28iLCJmaWxlcGF0aCI6InB1bHNlL2xvZ2RiLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/filereader.go","value":"eyJkdXJhdGlvbiI6MTcxMDg4MTUwMCwiZmlsZW5hbWUiOiJmaWxlcmVhZGVyLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9naXQvZmlsZXJlYWRlci5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} +{"key":"2024-06-16_pulse_pulse/server/server_test.go","value":"eyJkdXJhdGlvbiI6MTUzMzk3NzAwMCwiZmlsZW5hbWUiOiJzZXJ2ZXJfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3NlcnZlcl90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6MjQxMzk5NDgzMywiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6NDc1Njc0MDEyNSwiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/restore.go","value":"eyJkdXJhdGlvbiI6MTExMTg0NDIwOCwiZmlsZW5hbWUiOiJyZXN0b3JlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXN0b3JlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Mzg0NzU5MzU4MywiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/filetypes.go","value":"eyJkdXJhdGlvbiI6MjAxMjMyMDI1MCwiZmlsZW5hbWUiOiJmaWxldHlwZXMuZ28iLCJmaWxlcGF0aCI6InB1bHNlL2dpdC9maWxldHlwZXMuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/restore.go","value":"eyJkdXJhdGlvbiI6MzI0OTA0OTI1MCwiZmlsZW5hbWUiOiJyZXN0b3JlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXN0b3JlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6MTY4MjcwMzQxNiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6NjE3MDc5NzM3NSwiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6MjU3NDQ1MzI1MCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6Njg5OTM4ODAwMCwiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6MjgxMzQyNTEyNSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6NzA5NTU3NTMzMywiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDAwNDY0NDY2NywiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NTQxODkxODkxNywiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git.go","value":"eyJkdXJhdGlvbiI6ODEwMzMzNzA5LCJmaWxlbmFtZSI6ImdpdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdC5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjQyNDIzMTA4NCwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDIwMTY3NjI1MCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjU4NDA1ODU0MiwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDM0MDc2OTA0MiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjY4OTkxMjc1MSwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDQ4NjgwMTMzNCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Njc5MDE3MDE2OCwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDYyNDQ4NjA0MywiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjkwODM2MjAwMiwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDc0NzY1MjA4NSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Njk5OTMwODMzNSwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NTk0NDI1MTkxOCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6NjY0MDkxMzM0LCJmaWxlbmFtZSI6InJlcG9zaXRvcnkuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3JlcG9zaXRvcnkuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjEwNjE3NjQ2MCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6Nzk2NjU5NjY4LCJmaWxlbmFtZSI6InJlcG9zaXRvcnkuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3JlcG9zaXRvcnkuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjIzMTcxMzY2OCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6OTEwMDA4MTY4LCJmaWxlbmFtZSI6InJlcG9zaXRvcnkuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3JlcG9zaXRvcnkuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjM2ODY4MDUwMSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTAxNjkyMjU0MywiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjUyMjU1NTA0MiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTEzMDA0OTg3NiwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjY1MjUyODI5MiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTIxODE5NTQxOCwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6Njc4Mjc0NjcwOSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTMxNjYyNDkxOCwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjkyNDIyNzY2NywiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTQyNTAyNDc1MSwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NzAzNDg5MjU4NCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6Mzc1NDM4NDY4MzQsImZpbGVuYW1lIjoiYWdncmVnYXRlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9zZXJ2ZXIvYWdncmVnYXRlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Nzc4MDc1MDE2OCwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/logdb.go","value":"eyJkdXJhdGlvbiI6ODE3MzEwNjQxMjUsImZpbGVuYW1lIjoibG9nZGIuZ28iLCJmaWxlcGF0aCI6InB1bHNlL2xvZ2RiLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} diff --git a/testdata/segments/aaaaaaaaaaaaaaab.log b/testdata/segments/three/aaaaaaaaaaaaaaab.log similarity index 100% rename from testdata/segments/aaaaaaaaaaaaaaab.log rename to testdata/segments/three/aaaaaaaaaaaaaaab.log diff --git a/testdata/segments/three/aaaaaaaaaaaaaaac.log b/testdata/segments/three/aaaaaaaaaaaaaaac.log new file mode 100644 index 0000000..53393f2 --- /dev/null +++ b/testdata/segments/three/aaaaaaaaaaaaaaac.log @@ -0,0 +1,4 @@ +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NTc2MTIwMTIyOTIsImZpbGVuYW1lIjoiYWdncmVnYXRlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9zZXJ2ZXIvYWdncmVnYXRlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjYyMzU2NzU5NTgsImZpbGVuYW1lIjoiYWdncmVnYXRlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9zZXJ2ZXIvYWdncmVnYXRlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/logdb_test.go","value":"eyJkdXJhdGlvbiI6Mzc0MDAxODU2NjcsImZpbGVuYW1lIjoibG9nZGJfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvbG9nZGJfdGVzdC5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} +{"key":"2024-06-16_pulse_pulse/logdb.go","value":"eyJkdXJhdGlvbiI6MTA3MjE0OTYyNTAwLCJmaWxlbmFtZSI6ImxvZ2RiLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9sb2dkYi5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} diff --git a/testdata/segments/two/aaaaaaaaaaaaaaaa.log b/testdata/segments/two/aaaaaaaaaaaaaaaa.log new file mode 100644 index 0000000..8c38a06 --- /dev/null +++ b/testdata/segments/two/aaaaaaaaaaaaaaaa.log @@ -0,0 +1,49 @@ +{"key":"2024-06-16_pulse_pulse/logdb.go","value":"eyJkdXJhdGlvbiI6NzMwNDU5MjgwMDAsImZpbGVuYW1lIjoibG9nZGIuZ28iLCJmaWxlcGF0aCI6InB1bHNlL2xvZ2RiLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/filereader.go","value":"eyJkdXJhdGlvbiI6MTcxMDg4MTUwMCwiZmlsZW5hbWUiOiJmaWxlcmVhZGVyLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9naXQvZmlsZXJlYWRlci5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} +{"key":"2024-06-16_pulse_pulse/server/server_test.go","value":"eyJkdXJhdGlvbiI6MTUzMzk3NzAwMCwiZmlsZW5hbWUiOiJzZXJ2ZXJfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3NlcnZlcl90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6MjQxMzk5NDgzMywiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6NDc1Njc0MDEyNSwiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/restore.go","value":"eyJkdXJhdGlvbiI6MTExMTg0NDIwOCwiZmlsZW5hbWUiOiJyZXN0b3JlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXN0b3JlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Mzg0NzU5MzU4MywiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/filetypes.go","value":"eyJkdXJhdGlvbiI6MjAxMjMyMDI1MCwiZmlsZW5hbWUiOiJmaWxldHlwZXMuZ28iLCJmaWxlcGF0aCI6InB1bHNlL2dpdC9maWxldHlwZXMuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/restore.go","value":"eyJkdXJhdGlvbiI6MzI0OTA0OTI1MCwiZmlsZW5hbWUiOiJyZXN0b3JlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXN0b3JlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6MTY4MjcwMzQxNiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6NjE3MDc5NzM3NSwiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6MjU3NDQ1MzI1MCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6Njg5OTM4ODAwMCwiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6MjgxMzQyNTEyNSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/proxy.go","value":"eyJkdXJhdGlvbiI6NzA5NTU3NTMzMywiZmlsZW5hbWUiOiJwcm94eS5nbyIsImZpbGVwYXRoIjoicHVsc2Uvc2VydmVyL3Byb3h5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDAwNDY0NDY2NywiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NTQxODkxODkxNywiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git.go","value":"eyJkdXJhdGlvbiI6ODEwMzMzNzA5LCJmaWxlbmFtZSI6ImdpdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdC5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjQyNDIzMTA4NCwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDIwMTY3NjI1MCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjU4NDA1ODU0MiwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDM0MDc2OTA0MiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjY4OTkxMjc1MSwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDQ4NjgwMTMzNCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Njc5MDE3MDE2OCwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDYyNDQ4NjA0MywiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6NjkwODM2MjAwMiwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NDc0NzY1MjA4NSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Njk5OTMwODMzNSwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NTk0NDI1MTkxOCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6NjY0MDkxMzM0LCJmaWxlbmFtZSI6InJlcG9zaXRvcnkuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3JlcG9zaXRvcnkuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjEwNjE3NjQ2MCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6Nzk2NjU5NjY4LCJmaWxlbmFtZSI6InJlcG9zaXRvcnkuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3JlcG9zaXRvcnkuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjIzMTcxMzY2OCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6OTEwMDA4MTY4LCJmaWxlbmFtZSI6InJlcG9zaXRvcnkuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3JlcG9zaXRvcnkuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjM2ODY4MDUwMSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTAxNjkyMjU0MywiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjUyMjU1NTA0MiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTEzMDA0OTg3NiwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjY1MjUyODI5MiwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTIxODE5NTQxOCwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6Njc4Mjc0NjcwOSwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTMxNjYyNDkxOCwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjkyNDIyNzY2NywiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/repository.go","value":"eyJkdXJhdGlvbiI6MTQyNTAyNDc1MSwiZmlsZW5hbWUiOiJyZXBvc2l0b3J5LmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9yZXBvc2l0b3J5LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NzAzNDg5MjU4NCwiZmlsZW5hbWUiOiJhZ2dyZWdhdGUuZ28iLCJmaWxlcGF0aCI6InB1bHNlL3NlcnZlci9hZ2dyZWdhdGUuZ28iLCJmaWxldHlwZSI6ImdvIiwicmVwb3NpdG9yeSI6InB1bHNlIn0="} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6Mzc1NDM4NDY4MzQsImZpbGVuYW1lIjoiYWdncmVnYXRlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9zZXJ2ZXIvYWdncmVnYXRlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/git/git_test.go","value":"eyJkdXJhdGlvbiI6Nzc4MDc1MDE2OCwiZmlsZW5hbWUiOiJnaXRfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvZ2l0L2dpdF90ZXN0LmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/logdb.go","value":"eyJkdXJhdGlvbiI6ODE3MzEwNjQxMjUsImZpbGVuYW1lIjoibG9nZGIuZ28iLCJmaWxlcGF0aCI6InB1bHNlL2xvZ2RiLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} diff --git a/testdata/segments/two/aaaaaaaaaaaaaaab.log b/testdata/segments/two/aaaaaaaaaaaaaaab.log new file mode 100644 index 0000000..53393f2 --- /dev/null +++ b/testdata/segments/two/aaaaaaaaaaaaaaab.log @@ -0,0 +1,4 @@ +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NTc2MTIwMTIyOTIsImZpbGVuYW1lIjoiYWdncmVnYXRlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9zZXJ2ZXIvYWdncmVnYXRlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/server/aggregate.go","value":"eyJkdXJhdGlvbiI6NjYyMzU2NzU5NTgsImZpbGVuYW1lIjoiYWdncmVnYXRlLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9zZXJ2ZXIvYWdncmVnYXRlLmdvIiwiZmlsZXR5cGUiOiJnbyIsInJlcG9zaXRvcnkiOiJwdWxzZSJ9"} +{"key":"2024-06-16_pulse_pulse/logdb_test.go","value":"eyJkdXJhdGlvbiI6Mzc0MDAxODU2NjcsImZpbGVuYW1lIjoibG9nZGJfdGVzdC5nbyIsImZpbGVwYXRoIjoicHVsc2UvbG9nZGJfdGVzdC5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} +{"key":"2024-06-16_pulse_pulse/logdb.go","value":"eyJkdXJhdGlvbiI6MTA3MjE0OTYyNTAwLCJmaWxlbmFtZSI6ImxvZ2RiLmdvIiwiZmlsZXBhdGgiOiJwdWxzZS9sb2dkYi5nbyIsImZpbGV0eXBlIjoiZ28iLCJyZXBvc2l0b3J5IjoicHVsc2UifQ=="} diff --git a/truncate_test.go b/truncate_test.go index fd498fb..e2ff349 100644 --- a/truncate_test.go +++ b/truncate_test.go @@ -8,8 +8,6 @@ import ( ) func TestTruncate(t *testing.T) { - t.Parallel() - loc, err := time.LoadLocation("Europe/Stockholm") if err != nil { t.Fatal("Failed to load Stockholm timezone:", err)