aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/github.com/allegro/bigcache/queue/bytes_queue.go
blob: 0285c72cd50f8fe77ae349e4b0d386f3f8f6be09 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
package queue

import (
    "encoding/binary"
    "log"
    "time"
)

const (
    // Number of bytes used to keep information about entry size
    headerEntrySize = 4
    // Bytes before left margin are not used. Zero index means element does not exist in queue, useful while reading slice from index
    leftMarginIndex = 1
    // Minimum empty blob size in bytes. Empty blob fills space between tail and head in additional memory allocation.
    // It keeps entries indexes unchanged
    minimumEmptyBlobSize = 32 + headerEntrySize
)

// BytesQueue is a non-thread safe queue type of fifo based on bytes array.
// For every push operation index of entry is returned. It can be used to read the entry later
type BytesQueue struct {
    array           []byte
    capacity        int
    maxCapacity     int
    head            int
    tail            int
    count           int
    rightMargin     int
    headerBuffer    []byte
    verbose         bool
    initialCapacity int
}

type queueError struct {
    message string
}

// NewBytesQueue initialize new bytes queue.
// Initial capacity is used in bytes array allocation
// When verbose flag is set then information about memory allocation are printed
func NewBytesQueue(initialCapacity int, maxCapacity int, verbose bool) *BytesQueue {
    return &BytesQueue{
        array:           make([]byte, initialCapacity),
        capacity:        initialCapacity,
        maxCapacity:     maxCapacity,
        headerBuffer:    make([]byte, headerEntrySize),
        tail:            leftMarginIndex,
        head:            leftMarginIndex,
        rightMargin:     leftMarginIndex,
        verbose:         verbose,
        initialCapacity: initialCapacity,
    }
}

// Reset removes all entries from queue
func (q *BytesQueue) Reset() {
    // Just reset indexes
    q.tail = leftMarginIndex
    q.head = leftMarginIndex
    q.rightMargin = leftMarginIndex
    q.count = 0
}

// Push copies entry at the end of queue and moves tail pointer. Allocates more space if needed.
// Returns index for pushed data or error if maximum size queue limit is reached.
func (q *BytesQueue) Push(data []byte) (int, error) {
    dataLen := len(data)

    if q.availableSpaceAfterTail() < dataLen+headerEntrySize {
        if q.availableSpaceBeforeHead() >= dataLen+headerEntrySize {
            q.tail = leftMarginIndex
        } else if q.capacity+headerEntrySize+dataLen >= q.maxCapacity && q.maxCapacity > 0 {
            return -1, &queueError{"Full queue. Maximum size limit reached."}
        } else {
            q.allocateAdditionalMemory(dataLen + headerEntrySize)
        }
    }

    index := q.tail

    q.push(data, dataLen)

    return index, nil
}

func (q *BytesQueue) allocateAdditionalMemory(minimum int) {
    start := time.Now()
    if q.capacity < minimum {
        q.capacity += minimum
    }
    q.capacity = q.capacity * 2
    if q.capacity > q.maxCapacity && q.maxCapacity > 0 {
        q.capacity = q.maxCapacity
    }

    oldArray := q.array
    q.array = make([]byte, q.capacity)

    if leftMarginIndex != q.rightMargin {
        copy(q.array, oldArray[:q.rightMargin])

        if q.tail < q.head {
            emptyBlobLen := q.head - q.tail - headerEntrySize
            q.push(make([]byte, emptyBlobLen), emptyBlobLen)
            q.head = leftMarginIndex
            q.tail = q.rightMargin
        }
    }

    if q.verbose {
        log.Printf("Allocated new queue in %s; Capacity: %d \n", time.Since(start), q.capacity)
    }
}

func (q *BytesQueue) push(data []byte, len int) {
    binary.LittleEndian.PutUint32(q.headerBuffer, uint32(len))
    q.copy(q.headerBuffer, headerEntrySize)

    q.copy(data, len)

    if q.tail > q.head {
        q.rightMargin = q.tail
    }

    q.count++
}

func (q *BytesQueue) copy(data []byte, len int) {
    q.tail += copy(q.array[q.tail:], data[:len])
}

// Pop reads the oldest entry from queue and moves head pointer to the next one
func (q *BytesQueue) Pop() ([]byte, error) {
    data, size, err := q.peek(q.head)
    if err != nil {
        return nil, err
    }

    q.head += headerEntrySize + size
    q.count--

    if q.head == q.rightMargin {
        q.head = leftMarginIndex
        if q.tail == q.rightMargin {
            q.tail = leftMarginIndex
        }
        q.rightMargin = q.tail
    }

    return data, nil
}

// Peek reads the oldest entry from list without moving head pointer
func (q *BytesQueue) Peek() ([]byte, error) {
    data, _, err := q.peek(q.head)
    return data, err
}

// Get reads entry from index
func (q *BytesQueue) Get(index int) ([]byte, error) {
    data, _, err := q.peek(index)
    return data, err
}

// Capacity returns number of allocated bytes for queue
func (q *BytesQueue) Capacity() int {
    return q.capacity
}

// Len returns number of entries kept in queue
func (q *BytesQueue) Len() int {
    return q.count
}

// Error returns error message
func (e *queueError) Error() string {
    return e.message
}

func (q *BytesQueue) peek(index int) ([]byte, int, error) {

    if q.count == 0 {
        return nil, 0, &queueError{"Empty queue"}
    }

    if index <= 0 {
        return nil, 0, &queueError{"Index must be grater than zero. Invalid index."}
    }

    if index+headerEntrySize >= len(q.array) {
        return nil, 0, &queueError{"Index out of range"}
    }

    blockSize := int(binary.LittleEndian.Uint32(q.array[index : index+headerEntrySize]))
    return q.array[index+headerEntrySize : index+headerEntrySize+blockSize], blockSize, nil
}

func (q *BytesQueue) availableSpaceAfterTail() int {
    if q.tail >= q.head {
        return q.capacity - q.tail
    }
    return q.head - q.tail - minimumEmptyBlobSize
}

func (q *BytesQueue) availableSpaceBeforeHead() int {
    if q.tail >= q.head {
        return q.head - leftMarginIndex - minimumEmptyBlobSize
    }
    return q.head - q.tail - minimumEmptyBlobSize
}