142 lines
4.0 KiB
Go
142 lines
4.0 KiB
Go
|
// Copyright 2022 The Prometheus Authors
|
||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
// you may not use this file except in compliance with the License.
|
||
|
// You may obtain a copy of the License at
|
||
|
//
|
||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||
|
//
|
||
|
// Unless required by applicable law or agreed to in writing, software
|
||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
// See the License for the specific language governing permissions and
|
||
|
// limitations under the License.
|
||
|
|
||
|
package chunks
|
||
|
|
||
|
import "sync"
|
||
|
|
||
|
// writeJobQueue is similar to buffered channel of chunkWriteJob, but manages its own buffers
|
||
|
// to avoid using a lot of memory when it's empty. It does that by storing elements into segments
|
||
|
// of equal size (segmentSize). When segment is not used anymore, reference to it are removed,
|
||
|
// so it can be treated as a garbage.
|
||
|
type writeJobQueue struct {
|
||
|
maxSize int
|
||
|
segmentSize int
|
||
|
|
||
|
mtx sync.Mutex // protects all following variables
|
||
|
pushed, popped *sync.Cond // signalled when something is pushed into the queue or popped from it
|
||
|
first, last *writeJobQueueSegment // pointer to first and last segment, if any
|
||
|
size int // total size of the queue
|
||
|
closed bool // after closing the queue, nothing can be pushed to it
|
||
|
}
|
||
|
|
||
|
type writeJobQueueSegment struct {
|
||
|
segment []chunkWriteJob
|
||
|
nextRead, nextWrite int // index of next read and next write in this segment.
|
||
|
nextSegment *writeJobQueueSegment // next segment, if any
|
||
|
}
|
||
|
|
||
|
func newWriteJobQueue(maxSize, segmentSize int) *writeJobQueue {
|
||
|
if maxSize <= 0 || segmentSize <= 0 {
|
||
|
panic("invalid queue")
|
||
|
}
|
||
|
|
||
|
q := &writeJobQueue{
|
||
|
maxSize: maxSize,
|
||
|
segmentSize: segmentSize,
|
||
|
}
|
||
|
|
||
|
q.pushed = sync.NewCond(&q.mtx)
|
||
|
q.popped = sync.NewCond(&q.mtx)
|
||
|
return q
|
||
|
}
|
||
|
|
||
|
func (q *writeJobQueue) close() {
|
||
|
q.mtx.Lock()
|
||
|
defer q.mtx.Unlock()
|
||
|
|
||
|
q.closed = true
|
||
|
|
||
|
// Unblock all blocked goroutines.
|
||
|
q.pushed.Broadcast()
|
||
|
q.popped.Broadcast()
|
||
|
}
|
||
|
|
||
|
// push blocks until there is space available in the queue, and then adds job to the queue.
|
||
|
// If queue is closed or gets closed while waiting for space, push returns false.
|
||
|
func (q *writeJobQueue) push(job chunkWriteJob) bool {
|
||
|
q.mtx.Lock()
|
||
|
defer q.mtx.Unlock()
|
||
|
|
||
|
// Wait until queue has more space or is closed.
|
||
|
for !q.closed && q.size >= q.maxSize {
|
||
|
q.popped.Wait()
|
||
|
}
|
||
|
|
||
|
if q.closed {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// Check if this segment has more space for writing, and create new one if not.
|
||
|
if q.last == nil || q.last.nextWrite >= q.segmentSize {
|
||
|
prevLast := q.last
|
||
|
q.last = &writeJobQueueSegment{
|
||
|
segment: make([]chunkWriteJob, q.segmentSize),
|
||
|
}
|
||
|
|
||
|
if prevLast != nil {
|
||
|
prevLast.nextSegment = q.last
|
||
|
}
|
||
|
if q.first == nil {
|
||
|
q.first = q.last
|
||
|
}
|
||
|
}
|
||
|
|
||
|
q.last.segment[q.last.nextWrite] = job
|
||
|
q.last.nextWrite++
|
||
|
q.size++
|
||
|
q.pushed.Signal()
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// pop returns first job from the queue, and true.
|
||
|
// If queue is empty, pop blocks until there is a job (returns true), or until queue is closed (returns false).
|
||
|
// If queue was already closed, pop first returns all remaining elements from the queue (with true value), and only then returns false.
|
||
|
func (q *writeJobQueue) pop() (chunkWriteJob, bool) {
|
||
|
q.mtx.Lock()
|
||
|
defer q.mtx.Unlock()
|
||
|
|
||
|
// wait until something is pushed to the queue, or queue is closed.
|
||
|
for q.size == 0 {
|
||
|
if q.closed {
|
||
|
return chunkWriteJob{}, false
|
||
|
}
|
||
|
|
||
|
q.pushed.Wait()
|
||
|
}
|
||
|
|
||
|
res := q.first.segment[q.first.nextRead]
|
||
|
q.first.segment[q.first.nextRead] = chunkWriteJob{} // clear just-read element
|
||
|
q.first.nextRead++
|
||
|
q.size--
|
||
|
|
||
|
// If we have read all possible elements from first segment, we can drop it.
|
||
|
if q.first.nextRead >= q.segmentSize {
|
||
|
q.first = q.first.nextSegment
|
||
|
if q.first == nil {
|
||
|
q.last = nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
q.popped.Signal()
|
||
|
return res, true
|
||
|
}
|
||
|
|
||
|
// length returns number of all jobs in the queue.
|
||
|
func (q *writeJobQueue) length() int {
|
||
|
q.mtx.Lock()
|
||
|
defer q.mtx.Unlock()
|
||
|
|
||
|
return q.size
|
||
|
}
|