parent
442f9f5b60
commit
a5c218883d
@ -1,22 +0,0 @@
|
|||||||
Copyright (c) 2016 Caleb Spare
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,69 +0,0 @@
|
|||||||
# xxhash
|
|
||||||
|
|
||||||
[![Go Reference](https://pkg.go.dev/badge/github.com/cespare/xxhash/v2.svg)](https://pkg.go.dev/github.com/cespare/xxhash/v2)
|
|
||||||
[![Test](https://github.com/cespare/xxhash/actions/workflows/test.yml/badge.svg)](https://github.com/cespare/xxhash/actions/workflows/test.yml)
|
|
||||||
|
|
||||||
xxhash is a Go implementation of the 64-bit
|
|
||||||
[xxHash](http://cyan4973.github.io/xxHash/) algorithm, XXH64. This is a
|
|
||||||
high-quality hashing algorithm that is much faster than anything in the Go
|
|
||||||
standard library.
|
|
||||||
|
|
||||||
This package provides a straightforward API:
|
|
||||||
|
|
||||||
```
|
|
||||||
func Sum64(b []byte) uint64
|
|
||||||
func Sum64String(s string) uint64
|
|
||||||
type Digest struct{ ... }
|
|
||||||
func New() *Digest
|
|
||||||
```
|
|
||||||
|
|
||||||
The `Digest` type implements hash.Hash64. Its key methods are:
|
|
||||||
|
|
||||||
```
|
|
||||||
func (*Digest) Write([]byte) (int, error)
|
|
||||||
func (*Digest) WriteString(string) (int, error)
|
|
||||||
func (*Digest) Sum64() uint64
|
|
||||||
```
|
|
||||||
|
|
||||||
This implementation provides a fast pure-Go implementation and an even faster
|
|
||||||
assembly implementation for amd64.
|
|
||||||
|
|
||||||
## Compatibility
|
|
||||||
|
|
||||||
This package is in a module and the latest code is in version 2 of the module.
|
|
||||||
You need a version of Go with at least "minimal module compatibility" to use
|
|
||||||
github.com/cespare/xxhash/v2:
|
|
||||||
|
|
||||||
* 1.9.7+ for Go 1.9
|
|
||||||
* 1.10.3+ for Go 1.10
|
|
||||||
* Go 1.11 or later
|
|
||||||
|
|
||||||
I recommend using the latest release of Go.
|
|
||||||
|
|
||||||
## Benchmarks
|
|
||||||
|
|
||||||
Here are some quick benchmarks comparing the pure-Go and assembly
|
|
||||||
implementations of Sum64.
|
|
||||||
|
|
||||||
| input size | purego | asm |
|
|
||||||
| --- | --- | --- |
|
|
||||||
| 5 B | 979.66 MB/s | 1291.17 MB/s |
|
|
||||||
| 100 B | 7475.26 MB/s | 7973.40 MB/s |
|
|
||||||
| 4 KB | 17573.46 MB/s | 17602.65 MB/s |
|
|
||||||
| 10 MB | 17131.46 MB/s | 17142.16 MB/s |
|
|
||||||
|
|
||||||
These numbers were generated on Ubuntu 18.04 with an Intel i7-8700K CPU using
|
|
||||||
the following commands under Go 1.11.2:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ go test -tags purego -benchtime 10s -bench '/xxhash,direct,bytes'
|
|
||||||
$ go test -benchtime 10s -bench '/xxhash,direct,bytes'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Projects using this package
|
|
||||||
|
|
||||||
- [InfluxDB](https://github.com/influxdata/influxdb)
|
|
||||||
- [Prometheus](https://github.com/prometheus/prometheus)
|
|
||||||
- [VictoriaMetrics](https://github.com/VictoriaMetrics/VictoriaMetrics)
|
|
||||||
- [FreeCache](https://github.com/coocood/freecache)
|
|
||||||
- [FastCache](https://github.com/VictoriaMetrics/fastcache)
|
|
@ -1,235 +0,0 @@
|
|||||||
// Package xxhash implements the 64-bit variant of xxHash (XXH64) as described
|
|
||||||
// at http://cyan4973.github.io/xxHash/.
|
|
||||||
package xxhash
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"math/bits"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
prime1 uint64 = 11400714785074694791
|
|
||||||
prime2 uint64 = 14029467366897019727
|
|
||||||
prime3 uint64 = 1609587929392839161
|
|
||||||
prime4 uint64 = 9650029242287828579
|
|
||||||
prime5 uint64 = 2870177450012600261
|
|
||||||
)
|
|
||||||
|
|
||||||
// NOTE(caleb): I'm using both consts and vars of the primes. Using consts where
|
|
||||||
// possible in the Go code is worth a small (but measurable) performance boost
|
|
||||||
// by avoiding some MOVQs. Vars are needed for the asm and also are useful for
|
|
||||||
// convenience in the Go code in a few places where we need to intentionally
|
|
||||||
// avoid constant arithmetic (e.g., v1 := prime1 + prime2 fails because the
|
|
||||||
// result overflows a uint64).
|
|
||||||
var (
|
|
||||||
prime1v = prime1
|
|
||||||
prime2v = prime2
|
|
||||||
prime3v = prime3
|
|
||||||
prime4v = prime4
|
|
||||||
prime5v = prime5
|
|
||||||
)
|
|
||||||
|
|
||||||
// Digest implements hash.Hash64.
|
|
||||||
type Digest struct {
|
|
||||||
v1 uint64
|
|
||||||
v2 uint64
|
|
||||||
v3 uint64
|
|
||||||
v4 uint64
|
|
||||||
total uint64
|
|
||||||
mem [32]byte
|
|
||||||
n int // how much of mem is used
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Digest that computes the 64-bit xxHash algorithm.
|
|
||||||
func New() *Digest {
|
|
||||||
var d Digest
|
|
||||||
d.Reset()
|
|
||||||
return &d
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset clears the Digest's state so that it can be reused.
|
|
||||||
func (d *Digest) Reset() {
|
|
||||||
d.v1 = prime1v + prime2
|
|
||||||
d.v2 = prime2
|
|
||||||
d.v3 = 0
|
|
||||||
d.v4 = -prime1v
|
|
||||||
d.total = 0
|
|
||||||
d.n = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Size always returns 8 bytes.
|
|
||||||
func (d *Digest) Size() int { return 8 }
|
|
||||||
|
|
||||||
// BlockSize always returns 32 bytes.
|
|
||||||
func (d *Digest) BlockSize() int { return 32 }
|
|
||||||
|
|
||||||
// Write adds more data to d. It always returns len(b), nil.
|
|
||||||
func (d *Digest) Write(b []byte) (n int, err error) {
|
|
||||||
n = len(b)
|
|
||||||
d.total += uint64(n)
|
|
||||||
|
|
||||||
if d.n+n < 32 {
|
|
||||||
// This new data doesn't even fill the current block.
|
|
||||||
copy(d.mem[d.n:], b)
|
|
||||||
d.n += n
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if d.n > 0 {
|
|
||||||
// Finish off the partial block.
|
|
||||||
copy(d.mem[d.n:], b)
|
|
||||||
d.v1 = round(d.v1, u64(d.mem[0:8]))
|
|
||||||
d.v2 = round(d.v2, u64(d.mem[8:16]))
|
|
||||||
d.v3 = round(d.v3, u64(d.mem[16:24]))
|
|
||||||
d.v4 = round(d.v4, u64(d.mem[24:32]))
|
|
||||||
b = b[32-d.n:]
|
|
||||||
d.n = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(b) >= 32 {
|
|
||||||
// One or more full blocks left.
|
|
||||||
nw := writeBlocks(d, b)
|
|
||||||
b = b[nw:]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store any remaining partial block.
|
|
||||||
copy(d.mem[:], b)
|
|
||||||
d.n = len(b)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sum appends the current hash to b and returns the resulting slice.
|
|
||||||
func (d *Digest) Sum(b []byte) []byte {
|
|
||||||
s := d.Sum64()
|
|
||||||
return append(
|
|
||||||
b,
|
|
||||||
byte(s>>56),
|
|
||||||
byte(s>>48),
|
|
||||||
byte(s>>40),
|
|
||||||
byte(s>>32),
|
|
||||||
byte(s>>24),
|
|
||||||
byte(s>>16),
|
|
||||||
byte(s>>8),
|
|
||||||
byte(s),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sum64 returns the current hash.
|
|
||||||
func (d *Digest) Sum64() uint64 {
|
|
||||||
var h uint64
|
|
||||||
|
|
||||||
if d.total >= 32 {
|
|
||||||
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
|
|
||||||
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
|
|
||||||
h = mergeRound(h, v1)
|
|
||||||
h = mergeRound(h, v2)
|
|
||||||
h = mergeRound(h, v3)
|
|
||||||
h = mergeRound(h, v4)
|
|
||||||
} else {
|
|
||||||
h = d.v3 + prime5
|
|
||||||
}
|
|
||||||
|
|
||||||
h += d.total
|
|
||||||
|
|
||||||
i, end := 0, d.n
|
|
||||||
for ; i+8 <= end; i += 8 {
|
|
||||||
k1 := round(0, u64(d.mem[i:i+8]))
|
|
||||||
h ^= k1
|
|
||||||
h = rol27(h)*prime1 + prime4
|
|
||||||
}
|
|
||||||
if i+4 <= end {
|
|
||||||
h ^= uint64(u32(d.mem[i:i+4])) * prime1
|
|
||||||
h = rol23(h)*prime2 + prime3
|
|
||||||
i += 4
|
|
||||||
}
|
|
||||||
for i < end {
|
|
||||||
h ^= uint64(d.mem[i]) * prime5
|
|
||||||
h = rol11(h) * prime1
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
|
|
||||||
h ^= h >> 33
|
|
||||||
h *= prime2
|
|
||||||
h ^= h >> 29
|
|
||||||
h *= prime3
|
|
||||||
h ^= h >> 32
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
magic = "xxh\x06"
|
|
||||||
marshaledSize = len(magic) + 8*5 + 32
|
|
||||||
)
|
|
||||||
|
|
||||||
// MarshalBinary implements the encoding.BinaryMarshaler interface.
|
|
||||||
func (d *Digest) MarshalBinary() ([]byte, error) {
|
|
||||||
b := make([]byte, 0, marshaledSize)
|
|
||||||
b = append(b, magic...)
|
|
||||||
b = appendUint64(b, d.v1)
|
|
||||||
b = appendUint64(b, d.v2)
|
|
||||||
b = appendUint64(b, d.v3)
|
|
||||||
b = appendUint64(b, d.v4)
|
|
||||||
b = appendUint64(b, d.total)
|
|
||||||
b = append(b, d.mem[:d.n]...)
|
|
||||||
b = b[:len(b)+len(d.mem)-d.n]
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface.
|
|
||||||
func (d *Digest) UnmarshalBinary(b []byte) error {
|
|
||||||
if len(b) < len(magic) || string(b[:len(magic)]) != magic {
|
|
||||||
return errors.New("xxhash: invalid hash state identifier")
|
|
||||||
}
|
|
||||||
if len(b) != marshaledSize {
|
|
||||||
return errors.New("xxhash: invalid hash state size")
|
|
||||||
}
|
|
||||||
b = b[len(magic):]
|
|
||||||
b, d.v1 = consumeUint64(b)
|
|
||||||
b, d.v2 = consumeUint64(b)
|
|
||||||
b, d.v3 = consumeUint64(b)
|
|
||||||
b, d.v4 = consumeUint64(b)
|
|
||||||
b, d.total = consumeUint64(b)
|
|
||||||
copy(d.mem[:], b)
|
|
||||||
d.n = int(d.total % uint64(len(d.mem)))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendUint64(b []byte, x uint64) []byte {
|
|
||||||
var a [8]byte
|
|
||||||
binary.LittleEndian.PutUint64(a[:], x)
|
|
||||||
return append(b, a[:]...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func consumeUint64(b []byte) ([]byte, uint64) {
|
|
||||||
x := u64(b)
|
|
||||||
return b[8:], x
|
|
||||||
}
|
|
||||||
|
|
||||||
func u64(b []byte) uint64 { return binary.LittleEndian.Uint64(b) }
|
|
||||||
func u32(b []byte) uint32 { return binary.LittleEndian.Uint32(b) }
|
|
||||||
|
|
||||||
func round(acc, input uint64) uint64 {
|
|
||||||
acc += input * prime2
|
|
||||||
acc = rol31(acc)
|
|
||||||
acc *= prime1
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeRound(acc, val uint64) uint64 {
|
|
||||||
val = round(0, val)
|
|
||||||
acc ^= val
|
|
||||||
acc = acc*prime1 + prime4
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
|
|
||||||
func rol1(x uint64) uint64 { return bits.RotateLeft64(x, 1) }
|
|
||||||
func rol7(x uint64) uint64 { return bits.RotateLeft64(x, 7) }
|
|
||||||
func rol11(x uint64) uint64 { return bits.RotateLeft64(x, 11) }
|
|
||||||
func rol12(x uint64) uint64 { return bits.RotateLeft64(x, 12) }
|
|
||||||
func rol18(x uint64) uint64 { return bits.RotateLeft64(x, 18) }
|
|
||||||
func rol23(x uint64) uint64 { return bits.RotateLeft64(x, 23) }
|
|
||||||
func rol27(x uint64) uint64 { return bits.RotateLeft64(x, 27) }
|
|
||||||
func rol31(x uint64) uint64 { return bits.RotateLeft64(x, 31) }
|
|
@ -1,13 +0,0 @@
|
|||||||
// +build !appengine
|
|
||||||
// +build gc
|
|
||||||
// +build !purego
|
|
||||||
|
|
||||||
package xxhash
|
|
||||||
|
|
||||||
// Sum64 computes the 64-bit xxHash digest of b.
|
|
||||||
//
|
|
||||||
//go:noescape
|
|
||||||
func Sum64(b []byte) uint64
|
|
||||||
|
|
||||||
//go:noescape
|
|
||||||
func writeBlocks(d *Digest, b []byte) int
|
|
@ -1,215 +0,0 @@
|
|||||||
// +build !appengine
|
|
||||||
// +build gc
|
|
||||||
// +build !purego
|
|
||||||
|
|
||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// Register allocation:
|
|
||||||
// AX h
|
|
||||||
// SI pointer to advance through b
|
|
||||||
// DX n
|
|
||||||
// BX loop end
|
|
||||||
// R8 v1, k1
|
|
||||||
// R9 v2
|
|
||||||
// R10 v3
|
|
||||||
// R11 v4
|
|
||||||
// R12 tmp
|
|
||||||
// R13 prime1v
|
|
||||||
// R14 prime2v
|
|
||||||
// DI prime4v
|
|
||||||
|
|
||||||
// round reads from and advances the buffer pointer in SI.
|
|
||||||
// It assumes that R13 has prime1v and R14 has prime2v.
|
|
||||||
#define round(r) \
|
|
||||||
MOVQ (SI), R12 \
|
|
||||||
ADDQ $8, SI \
|
|
||||||
IMULQ R14, R12 \
|
|
||||||
ADDQ R12, r \
|
|
||||||
ROLQ $31, r \
|
|
||||||
IMULQ R13, r
|
|
||||||
|
|
||||||
// mergeRound applies a merge round on the two registers acc and val.
|
|
||||||
// It assumes that R13 has prime1v, R14 has prime2v, and DI has prime4v.
|
|
||||||
#define mergeRound(acc, val) \
|
|
||||||
IMULQ R14, val \
|
|
||||||
ROLQ $31, val \
|
|
||||||
IMULQ R13, val \
|
|
||||||
XORQ val, acc \
|
|
||||||
IMULQ R13, acc \
|
|
||||||
ADDQ DI, acc
|
|
||||||
|
|
||||||
// func Sum64(b []byte) uint64
|
|
||||||
TEXT ·Sum64(SB), NOSPLIT, $0-32
|
|
||||||
// Load fixed primes.
|
|
||||||
MOVQ ·prime1v(SB), R13
|
|
||||||
MOVQ ·prime2v(SB), R14
|
|
||||||
MOVQ ·prime4v(SB), DI
|
|
||||||
|
|
||||||
// Load slice.
|
|
||||||
MOVQ b_base+0(FP), SI
|
|
||||||
MOVQ b_len+8(FP), DX
|
|
||||||
LEAQ (SI)(DX*1), BX
|
|
||||||
|
|
||||||
// The first loop limit will be len(b)-32.
|
|
||||||
SUBQ $32, BX
|
|
||||||
|
|
||||||
// Check whether we have at least one block.
|
|
||||||
CMPQ DX, $32
|
|
||||||
JLT noBlocks
|
|
||||||
|
|
||||||
// Set up initial state (v1, v2, v3, v4).
|
|
||||||
MOVQ R13, R8
|
|
||||||
ADDQ R14, R8
|
|
||||||
MOVQ R14, R9
|
|
||||||
XORQ R10, R10
|
|
||||||
XORQ R11, R11
|
|
||||||
SUBQ R13, R11
|
|
||||||
|
|
||||||
// Loop until SI > BX.
|
|
||||||
blockLoop:
|
|
||||||
round(R8)
|
|
||||||
round(R9)
|
|
||||||
round(R10)
|
|
||||||
round(R11)
|
|
||||||
|
|
||||||
CMPQ SI, BX
|
|
||||||
JLE blockLoop
|
|
||||||
|
|
||||||
MOVQ R8, AX
|
|
||||||
ROLQ $1, AX
|
|
||||||
MOVQ R9, R12
|
|
||||||
ROLQ $7, R12
|
|
||||||
ADDQ R12, AX
|
|
||||||
MOVQ R10, R12
|
|
||||||
ROLQ $12, R12
|
|
||||||
ADDQ R12, AX
|
|
||||||
MOVQ R11, R12
|
|
||||||
ROLQ $18, R12
|
|
||||||
ADDQ R12, AX
|
|
||||||
|
|
||||||
mergeRound(AX, R8)
|
|
||||||
mergeRound(AX, R9)
|
|
||||||
mergeRound(AX, R10)
|
|
||||||
mergeRound(AX, R11)
|
|
||||||
|
|
||||||
JMP afterBlocks
|
|
||||||
|
|
||||||
noBlocks:
|
|
||||||
MOVQ ·prime5v(SB), AX
|
|
||||||
|
|
||||||
afterBlocks:
|
|
||||||
ADDQ DX, AX
|
|
||||||
|
|
||||||
// Right now BX has len(b)-32, and we want to loop until SI > len(b)-8.
|
|
||||||
ADDQ $24, BX
|
|
||||||
|
|
||||||
CMPQ SI, BX
|
|
||||||
JG fourByte
|
|
||||||
|
|
||||||
wordLoop:
|
|
||||||
// Calculate k1.
|
|
||||||
MOVQ (SI), R8
|
|
||||||
ADDQ $8, SI
|
|
||||||
IMULQ R14, R8
|
|
||||||
ROLQ $31, R8
|
|
||||||
IMULQ R13, R8
|
|
||||||
|
|
||||||
XORQ R8, AX
|
|
||||||
ROLQ $27, AX
|
|
||||||
IMULQ R13, AX
|
|
||||||
ADDQ DI, AX
|
|
||||||
|
|
||||||
CMPQ SI, BX
|
|
||||||
JLE wordLoop
|
|
||||||
|
|
||||||
fourByte:
|
|
||||||
ADDQ $4, BX
|
|
||||||
CMPQ SI, BX
|
|
||||||
JG singles
|
|
||||||
|
|
||||||
MOVL (SI), R8
|
|
||||||
ADDQ $4, SI
|
|
||||||
IMULQ R13, R8
|
|
||||||
XORQ R8, AX
|
|
||||||
|
|
||||||
ROLQ $23, AX
|
|
||||||
IMULQ R14, AX
|
|
||||||
ADDQ ·prime3v(SB), AX
|
|
||||||
|
|
||||||
singles:
|
|
||||||
ADDQ $4, BX
|
|
||||||
CMPQ SI, BX
|
|
||||||
JGE finalize
|
|
||||||
|
|
||||||
singlesLoop:
|
|
||||||
MOVBQZX (SI), R12
|
|
||||||
ADDQ $1, SI
|
|
||||||
IMULQ ·prime5v(SB), R12
|
|
||||||
XORQ R12, AX
|
|
||||||
|
|
||||||
ROLQ $11, AX
|
|
||||||
IMULQ R13, AX
|
|
||||||
|
|
||||||
CMPQ SI, BX
|
|
||||||
JL singlesLoop
|
|
||||||
|
|
||||||
finalize:
|
|
||||||
MOVQ AX, R12
|
|
||||||
SHRQ $33, R12
|
|
||||||
XORQ R12, AX
|
|
||||||
IMULQ R14, AX
|
|
||||||
MOVQ AX, R12
|
|
||||||
SHRQ $29, R12
|
|
||||||
XORQ R12, AX
|
|
||||||
IMULQ ·prime3v(SB), AX
|
|
||||||
MOVQ AX, R12
|
|
||||||
SHRQ $32, R12
|
|
||||||
XORQ R12, AX
|
|
||||||
|
|
||||||
MOVQ AX, ret+24(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
// writeBlocks uses the same registers as above except that it uses AX to store
|
|
||||||
// the d pointer.
|
|
||||||
|
|
||||||
// func writeBlocks(d *Digest, b []byte) int
|
|
||||||
TEXT ·writeBlocks(SB), NOSPLIT, $0-40
|
|
||||||
// Load fixed primes needed for round.
|
|
||||||
MOVQ ·prime1v(SB), R13
|
|
||||||
MOVQ ·prime2v(SB), R14
|
|
||||||
|
|
||||||
// Load slice.
|
|
||||||
MOVQ b_base+8(FP), SI
|
|
||||||
MOVQ b_len+16(FP), DX
|
|
||||||
LEAQ (SI)(DX*1), BX
|
|
||||||
SUBQ $32, BX
|
|
||||||
|
|
||||||
// Load vN from d.
|
|
||||||
MOVQ d+0(FP), AX
|
|
||||||
MOVQ 0(AX), R8 // v1
|
|
||||||
MOVQ 8(AX), R9 // v2
|
|
||||||
MOVQ 16(AX), R10 // v3
|
|
||||||
MOVQ 24(AX), R11 // v4
|
|
||||||
|
|
||||||
// We don't need to check the loop condition here; this function is
|
|
||||||
// always called with at least one block of data to process.
|
|
||||||
blockLoop:
|
|
||||||
round(R8)
|
|
||||||
round(R9)
|
|
||||||
round(R10)
|
|
||||||
round(R11)
|
|
||||||
|
|
||||||
CMPQ SI, BX
|
|
||||||
JLE blockLoop
|
|
||||||
|
|
||||||
// Copy vN back to d.
|
|
||||||
MOVQ R8, 0(AX)
|
|
||||||
MOVQ R9, 8(AX)
|
|
||||||
MOVQ R10, 16(AX)
|
|
||||||
MOVQ R11, 24(AX)
|
|
||||||
|
|
||||||
// The number of bytes written is SI minus the old base pointer.
|
|
||||||
SUBQ b_base+8(FP), SI
|
|
||||||
MOVQ SI, ret+32(FP)
|
|
||||||
|
|
||||||
RET
|
|
@ -1,76 +0,0 @@
|
|||||||
// +build !amd64 appengine !gc purego
|
|
||||||
|
|
||||||
package xxhash
|
|
||||||
|
|
||||||
// Sum64 computes the 64-bit xxHash digest of b.
|
|
||||||
func Sum64(b []byte) uint64 {
|
|
||||||
// A simpler version would be
|
|
||||||
// d := New()
|
|
||||||
// d.Write(b)
|
|
||||||
// return d.Sum64()
|
|
||||||
// but this is faster, particularly for small inputs.
|
|
||||||
|
|
||||||
n := len(b)
|
|
||||||
var h uint64
|
|
||||||
|
|
||||||
if n >= 32 {
|
|
||||||
v1 := prime1v + prime2
|
|
||||||
v2 := prime2
|
|
||||||
v3 := uint64(0)
|
|
||||||
v4 := -prime1v
|
|
||||||
for len(b) >= 32 {
|
|
||||||
v1 = round(v1, u64(b[0:8:len(b)]))
|
|
||||||
v2 = round(v2, u64(b[8:16:len(b)]))
|
|
||||||
v3 = round(v3, u64(b[16:24:len(b)]))
|
|
||||||
v4 = round(v4, u64(b[24:32:len(b)]))
|
|
||||||
b = b[32:len(b):len(b)]
|
|
||||||
}
|
|
||||||
h = rol1(v1) + rol7(v2) + rol12(v3) + rol18(v4)
|
|
||||||
h = mergeRound(h, v1)
|
|
||||||
h = mergeRound(h, v2)
|
|
||||||
h = mergeRound(h, v3)
|
|
||||||
h = mergeRound(h, v4)
|
|
||||||
} else {
|
|
||||||
h = prime5
|
|
||||||
}
|
|
||||||
|
|
||||||
h += uint64(n)
|
|
||||||
|
|
||||||
i, end := 0, len(b)
|
|
||||||
for ; i+8 <= end; i += 8 {
|
|
||||||
k1 := round(0, u64(b[i:i+8:len(b)]))
|
|
||||||
h ^= k1
|
|
||||||
h = rol27(h)*prime1 + prime4
|
|
||||||
}
|
|
||||||
if i+4 <= end {
|
|
||||||
h ^= uint64(u32(b[i:i+4:len(b)])) * prime1
|
|
||||||
h = rol23(h)*prime2 + prime3
|
|
||||||
i += 4
|
|
||||||
}
|
|
||||||
for ; i < end; i++ {
|
|
||||||
h ^= uint64(b[i]) * prime5
|
|
||||||
h = rol11(h) * prime1
|
|
||||||
}
|
|
||||||
|
|
||||||
h ^= h >> 33
|
|
||||||
h *= prime2
|
|
||||||
h ^= h >> 29
|
|
||||||
h *= prime3
|
|
||||||
h ^= h >> 32
|
|
||||||
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeBlocks(d *Digest, b []byte) int {
|
|
||||||
v1, v2, v3, v4 := d.v1, d.v2, d.v3, d.v4
|
|
||||||
n := len(b)
|
|
||||||
for len(b) >= 32 {
|
|
||||||
v1 = round(v1, u64(b[0:8:len(b)]))
|
|
||||||
v2 = round(v2, u64(b[8:16:len(b)]))
|
|
||||||
v3 = round(v3, u64(b[16:24:len(b)]))
|
|
||||||
v4 = round(v4, u64(b[24:32:len(b)]))
|
|
||||||
b = b[32:len(b):len(b)]
|
|
||||||
}
|
|
||||||
d.v1, d.v2, d.v3, d.v4 = v1, v2, v3, v4
|
|
||||||
return n - len(b)
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
// +build appengine
|
|
||||||
|
|
||||||
// This file contains the safe implementations of otherwise unsafe-using code.
|
|
||||||
|
|
||||||
package xxhash
|
|
||||||
|
|
||||||
// Sum64String computes the 64-bit xxHash digest of s.
|
|
||||||
func Sum64String(s string) uint64 {
|
|
||||||
return Sum64([]byte(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteString adds more data to d. It always returns len(s), nil.
|
|
||||||
func (d *Digest) WriteString(s string) (n int, err error) {
|
|
||||||
return d.Write([]byte(s))
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
// +build !appengine
|
|
||||||
|
|
||||||
// This file encapsulates usage of unsafe.
|
|
||||||
// xxhash_safe.go contains the safe implementations.
|
|
||||||
|
|
||||||
package xxhash
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// In the future it's possible that compiler optimizations will make these
|
|
||||||
// XxxString functions unnecessary by realizing that calls such as
|
|
||||||
// Sum64([]byte(s)) don't need to copy s. See https://golang.org/issue/2205.
|
|
||||||
// If that happens, even if we keep these functions they can be replaced with
|
|
||||||
// the trivial safe code.
|
|
||||||
|
|
||||||
// NOTE: The usual way of doing an unsafe string-to-[]byte conversion is:
|
|
||||||
//
|
|
||||||
// var b []byte
|
|
||||||
// bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
|
|
||||||
// bh.Data = (*reflect.StringHeader)(unsafe.Pointer(&s)).Data
|
|
||||||
// bh.Len = len(s)
|
|
||||||
// bh.Cap = len(s)
|
|
||||||
//
|
|
||||||
// Unfortunately, as of Go 1.15.3 the inliner's cost model assigns a high enough
|
|
||||||
// weight to this sequence of expressions that any function that uses it will
|
|
||||||
// not be inlined. Instead, the functions below use a different unsafe
|
|
||||||
// conversion designed to minimize the inliner weight and allow both to be
|
|
||||||
// inlined. There is also a test (TestInlining) which verifies that these are
|
|
||||||
// inlined.
|
|
||||||
//
|
|
||||||
// See https://github.com/golang/go/issues/42739 for discussion.
|
|
||||||
|
|
||||||
// Sum64String computes the 64-bit xxHash digest of s.
|
|
||||||
// It may be faster than Sum64([]byte(s)) by avoiding a copy.
|
|
||||||
func Sum64String(s string) uint64 {
|
|
||||||
b := *(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)}))
|
|
||||||
return Sum64(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteString adds more data to d. It always returns len(s), nil.
|
|
||||||
// It may be faster than Write([]byte(s)) by avoiding a copy.
|
|
||||||
func (d *Digest) WriteString(s string) (n int, err error) {
|
|
||||||
d.Write(*(*[]byte)(unsafe.Pointer(&sliceHeader{s, len(s)})))
|
|
||||||
// d.Write always returns len(s), nil.
|
|
||||||
// Ignoring the return output and returning these fixed values buys a
|
|
||||||
// savings of 6 in the inliner's cost model.
|
|
||||||
return len(s), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sliceHeader is similar to reflect.SliceHeader, but it assumes that the layout
|
|
||||||
// of the first two words is the same as the layout of a string.
|
|
||||||
type sliceHeader struct {
|
|
||||||
s string
|
|
||||||
cap int
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2017-2020 Damian Gryski <damian@gryski.com>
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in
|
|
||||||
all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
||||||
THE SOFTWARE.
|
|
@ -1,79 +0,0 @@
|
|||||||
package rendezvous
|
|
||||||
|
|
||||||
type Rendezvous struct {
|
|
||||||
nodes map[string]int
|
|
||||||
nstr []string
|
|
||||||
nhash []uint64
|
|
||||||
hash Hasher
|
|
||||||
}
|
|
||||||
|
|
||||||
type Hasher func(s string) uint64
|
|
||||||
|
|
||||||
func New(nodes []string, hash Hasher) *Rendezvous {
|
|
||||||
r := &Rendezvous{
|
|
||||||
nodes: make(map[string]int, len(nodes)),
|
|
||||||
nstr: make([]string, len(nodes)),
|
|
||||||
nhash: make([]uint64, len(nodes)),
|
|
||||||
hash: hash,
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, n := range nodes {
|
|
||||||
r.nodes[n] = i
|
|
||||||
r.nstr[i] = n
|
|
||||||
r.nhash[i] = hash(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rendezvous) Lookup(k string) string {
|
|
||||||
// short-circuit if we're empty
|
|
||||||
if len(r.nodes) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
khash := r.hash(k)
|
|
||||||
|
|
||||||
var midx int
|
|
||||||
var mhash = xorshiftMult64(khash ^ r.nhash[0])
|
|
||||||
|
|
||||||
for i, nhash := range r.nhash[1:] {
|
|
||||||
if h := xorshiftMult64(khash ^ nhash); h > mhash {
|
|
||||||
midx = i + 1
|
|
||||||
mhash = h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.nstr[midx]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rendezvous) Add(node string) {
|
|
||||||
r.nodes[node] = len(r.nstr)
|
|
||||||
r.nstr = append(r.nstr, node)
|
|
||||||
r.nhash = append(r.nhash, r.hash(node))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Rendezvous) Remove(node string) {
|
|
||||||
// find index of node to remove
|
|
||||||
nidx := r.nodes[node]
|
|
||||||
|
|
||||||
// remove from the slices
|
|
||||||
l := len(r.nstr)
|
|
||||||
r.nstr[nidx] = r.nstr[l]
|
|
||||||
r.nstr = r.nstr[:l]
|
|
||||||
|
|
||||||
r.nhash[nidx] = r.nhash[l]
|
|
||||||
r.nhash = r.nhash[:l]
|
|
||||||
|
|
||||||
// update the map
|
|
||||||
delete(r.nodes, node)
|
|
||||||
moved := r.nstr[nidx]
|
|
||||||
r.nodes[moved] = nidx
|
|
||||||
}
|
|
||||||
|
|
||||||
func xorshiftMult64(x uint64) uint64 {
|
|
||||||
x ^= x >> 12 // a
|
|
||||||
x ^= x << 25 // b
|
|
||||||
x ^= x >> 27 // c
|
|
||||||
return x * 2685821657736338717
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 茂名聚合科技有限公司
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
@ -1,139 +0,0 @@
|
|||||||
package gohttp
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"gopkg.in/dtapps/go-library.v3/utils/gorequest"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
Status string
|
|
||||||
StatusCode int
|
|
||||||
Header http.Header
|
|
||||||
Body []byte
|
|
||||||
ContentLength int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func Get(url string, params map[string]interface{}) (httpResponse Response, err error) {
|
|
||||||
// 创建 http 客户端
|
|
||||||
client := &http.Client{}
|
|
||||||
// 创建请求
|
|
||||||
req, _ := http.NewRequest(http.MethodGet, url, nil)
|
|
||||||
if len(params) > 0 {
|
|
||||||
// GET 请求携带查询参数
|
|
||||||
q := req.URL.Query()
|
|
||||||
for k, v := range params {
|
|
||||||
q.Add(k, getString(v))
|
|
||||||
}
|
|
||||||
req.URL.RawQuery = q.Encode()
|
|
||||||
}
|
|
||||||
// 设置请求头
|
|
||||||
req.Header.Set("User-Agent", gorequest.GetRandomUserAgent())
|
|
||||||
// 发送请求
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
// 格式化返回错误
|
|
||||||
return httpResponse, errors.New(fmt.Sprintf("请求出错 %s", err))
|
|
||||||
}
|
|
||||||
// 最后关闭连接
|
|
||||||
defer resp.Body.Close()
|
|
||||||
// 读取内容
|
|
||||||
respBody, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return httpResponse, errors.New(fmt.Sprintf("解析内容出错 %s", err))
|
|
||||||
}
|
|
||||||
httpResponse.Status = resp.Status
|
|
||||||
httpResponse.StatusCode = resp.StatusCode
|
|
||||||
httpResponse.Header = resp.Header
|
|
||||||
httpResponse.Body = respBody
|
|
||||||
httpResponse.ContentLength = resp.ContentLength
|
|
||||||
return httpResponse, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostForm(targetUrl string, params map[string]interface{}) (httpResponse Response, err error) {
|
|
||||||
// 创建 http 客户端
|
|
||||||
client := &http.Client{}
|
|
||||||
// 携带 form 参数
|
|
||||||
form := url.Values{}
|
|
||||||
if len(params) > 0 {
|
|
||||||
for k, v := range params {
|
|
||||||
form.Add(k, getString(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 创建请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPost, targetUrl, strings.NewReader(form.Encode()))
|
|
||||||
// 设置请求头
|
|
||||||
req.Header.Set("User-Agent", gorequest.GetRandomUserAgent())
|
|
||||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
||||||
// 发送请求
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
// 格式化返回错误
|
|
||||||
return httpResponse, errors.New(fmt.Sprintf("请求出错 %s", err))
|
|
||||||
}
|
|
||||||
// 最后关闭连接
|
|
||||||
defer resp.Body.Close()
|
|
||||||
// 读取内容
|
|
||||||
respBody, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return httpResponse, errors.New(fmt.Sprintf("解析内容出错 %s", err))
|
|
||||||
}
|
|
||||||
httpResponse.Status = resp.Status
|
|
||||||
httpResponse.StatusCode = resp.StatusCode
|
|
||||||
httpResponse.Header = resp.Header
|
|
||||||
httpResponse.Body = respBody
|
|
||||||
httpResponse.ContentLength = resp.ContentLength
|
|
||||||
return httpResponse, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func PostJson(targetUrl string, paramsStr []byte) (httpResponse Response, err error) {
|
|
||||||
// 创建请求
|
|
||||||
req, _ := http.NewRequest(http.MethodPost, targetUrl, bytes.NewBuffer(paramsStr))
|
|
||||||
// 设置请求头
|
|
||||||
req.Header.Set("User-Agent", gorequest.GetRandomUserAgent())
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
// 创建 http 客户端
|
|
||||||
client := &http.Client{}
|
|
||||||
// 发送请求
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
// 格式化返回错误
|
|
||||||
return httpResponse, errors.New(fmt.Sprintf("请求出错 %s", err))
|
|
||||||
}
|
|
||||||
// 最后关闭连接
|
|
||||||
defer resp.Body.Close()
|
|
||||||
// 读取内容
|
|
||||||
respBody, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return httpResponse, errors.New(fmt.Sprintf("解析内容出错 %s", err))
|
|
||||||
}
|
|
||||||
httpResponse.Status = resp.Status
|
|
||||||
httpResponse.StatusCode = resp.StatusCode
|
|
||||||
httpResponse.Header = resp.Header
|
|
||||||
httpResponse.Body = respBody
|
|
||||||
httpResponse.ContentLength = resp.ContentLength
|
|
||||||
return httpResponse, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func getString(i interface{}) string {
|
|
||||||
switch v := i.(type) {
|
|
||||||
case string:
|
|
||||||
return v
|
|
||||||
case []byte:
|
|
||||||
return string(v)
|
|
||||||
case int:
|
|
||||||
return strconv.Itoa(v)
|
|
||||||
case bool:
|
|
||||||
return strconv.FormatBool(v)
|
|
||||||
default:
|
|
||||||
marshal, _ := json.Marshal(v)
|
|
||||||
return string(marshal)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
package gomd5
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Php(str string) string {
|
|
||||||
h := md5.New()
|
|
||||||
io.WriteString(h, str)
|
|
||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func Md5(str string) string {
|
|
||||||
s := md5.New()
|
|
||||||
s.Write([]byte(str))
|
|
||||||
return hex.EncodeToString(s.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToUpper md5加密后转大写
|
|
||||||
func ToUpper(str string) string {
|
|
||||||
h := md5.New()
|
|
||||||
h.Write([]byte(str))
|
|
||||||
cipherStr := h.Sum(nil)
|
|
||||||
return strings.ToUpper(hex.EncodeToString(cipherStr))
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMD5Encode 返回一个32位md5加密后的字符串
|
|
||||||
func GetMD5Encode(data string) string {
|
|
||||||
h := md5.New()
|
|
||||||
h.Write([]byte(data))
|
|
||||||
return hex.EncodeToString(h.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get16MD5Encode 返回一个16位md5加密后的字符串
|
|
||||||
func Get16MD5Encode(data string) string {
|
|
||||||
return GetMD5Encode(data)[8:24]
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
package goparams
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/nilorg/sdk/convert"
|
|
||||||
"log"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Params map[string]interface{}
|
|
||||||
|
|
||||||
func NewParams() Params {
|
|
||||||
P := make(Params)
|
|
||||||
return P
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewParamsWithType(params ...Params) Params {
|
|
||||||
p := make(Params)
|
|
||||||
for _, v := range params {
|
|
||||||
p.SetParams(v)
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Params) Set(key string, value interface{}) {
|
|
||||||
p[key] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Params) SetParams(params Params) {
|
|
||||||
for key, value := range params {
|
|
||||||
p[key] = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p Params) GetQuery() string {
|
|
||||||
u := url.Values{}
|
|
||||||
for k, v := range p {
|
|
||||||
u.Set(k, GetParamsString(v))
|
|
||||||
}
|
|
||||||
return u.Encode()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetParamsString(src interface{}) string {
|
|
||||||
switch src.(type) {
|
|
||||||
case string:
|
|
||||||
return src.(string)
|
|
||||||
case int, int8, int32, int64:
|
|
||||||
case uint8, uint16, uint32, uint64:
|
|
||||||
case float32, float64:
|
|
||||||
return convert.ToString(src)
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(src)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
return string(data)
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
package gorandom
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const numbers string = "0123456789"
|
|
||||||
const letters string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
||||||
const specials = "~!@#$%^*()_+-=[]{}|;:,./<>?"
|
|
||||||
const alphanumerics string = letters + numbers
|
|
||||||
const ascii string = alphanumerics + specials
|
|
||||||
|
|
||||||
func random(n int, chars string) string {
|
|
||||||
if n <= 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
||||||
bytes := make([]byte, n, n)
|
|
||||||
l := len(chars)
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
bytes[i] = chars[r.Intn(l)]
|
|
||||||
}
|
|
||||||
return string(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alphanumeric 随机字母数字
|
|
||||||
func Alphanumeric(n int) string {
|
|
||||||
return random(n, alphanumerics)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Alphabetic 随机字母
|
|
||||||
func Alphabetic(n int) string {
|
|
||||||
return random(n, letters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numeric 随机数字
|
|
||||||
func Numeric(n int) string {
|
|
||||||
return random(n, numbers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ascii 随机ASCII
|
|
||||||
func Ascii(n int) string {
|
|
||||||
return random(n, ascii)
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
type Iterator struct {
|
|
||||||
data []interface{}
|
|
||||||
index int
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIterator 构造函数
|
|
||||||
func NewIterator(data []interface{}) *Iterator {
|
|
||||||
return &Iterator{data: data}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasNext 是否有下一个
|
|
||||||
func (i *Iterator) HasNext() bool {
|
|
||||||
if i.data == nil || len(i.data) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return i.index < len(i.data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next 循环下一个
|
|
||||||
func (i *Iterator) Next() (Ret interface{}) {
|
|
||||||
Ret = i.data[i.index]
|
|
||||||
i.index = i.index + 1
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type empty struct{}
|
|
||||||
|
|
||||||
const (
|
|
||||||
AttrExpr = "expr" // 过期时间
|
|
||||||
AttrNx = "nx" // 设置Nx
|
|
||||||
)
|
|
||||||
|
|
||||||
type OperationAttr struct {
|
|
||||||
Name string
|
|
||||||
Value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type OperationAttrs []*OperationAttr
|
|
||||||
|
|
||||||
func (a OperationAttrs) Find(name string) interface{} {
|
|
||||||
for _, attr := range a {
|
|
||||||
if attr.Name == name {
|
|
||||||
return attr.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithExpire 过期时间
|
|
||||||
func WithExpire(t time.Duration) *OperationAttr {
|
|
||||||
return &OperationAttr{Name: AttrExpr, Value: t}
|
|
||||||
}
|
|
||||||
|
|
||||||
func WithNX() *OperationAttr {
|
|
||||||
return &OperationAttr{Name: AttrNx, Value: empty{}}
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
SerializerJson = "json"
|
|
||||||
SerializerString = "string"
|
|
||||||
)
|
|
||||||
|
|
||||||
type JsonGttFunc func() interface{}
|
|
||||||
|
|
||||||
type DBGttFunc func() string
|
|
||||||
|
|
||||||
// SimpleCache 缓存
|
|
||||||
type SimpleCache struct {
|
|
||||||
Operation *StringOperation // 操作类
|
|
||||||
Expire time.Duration // 过去时间
|
|
||||||
DBGetter DBGttFunc // 缓存不存在的操作 DB
|
|
||||||
JsonGetter JsonGttFunc // 缓存不存在的操作 JSON
|
|
||||||
Serializer string // 序列化方式
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSimpleCache 构造函数
|
|
||||||
func (app *App) NewSimpleCache(operation *StringOperation, expire time.Duration, serializer string) *SimpleCache {
|
|
||||||
return &SimpleCache{
|
|
||||||
Operation: operation, // 操作类
|
|
||||||
Expire: expire, // 过去时间
|
|
||||||
Serializer: serializer, // 缓存不存在的操作 DB
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCache 设置缓存
|
|
||||||
func (c *SimpleCache) SetCache(key string, value interface{}) {
|
|
||||||
c.Operation.Set(key, value, WithExpire(c.Expire)).Unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCache 获取缓存
|
|
||||||
func (c *SimpleCache) GetCache(key string) (ret interface{}) {
|
|
||||||
if c.Serializer == SerializerJson {
|
|
||||||
f := func() string {
|
|
||||||
obj := c.JsonGetter()
|
|
||||||
b, err := json.Marshal(obj)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
ret = c.Operation.Get(key).UnwrapOrElse(f)
|
|
||||||
c.SetCache(key, ret)
|
|
||||||
} else if c.Serializer == SerializerString {
|
|
||||||
f := func() string {
|
|
||||||
return c.DBGetter()
|
|
||||||
}
|
|
||||||
ret = c.Operation.Get(key).UnwrapOrElse(f)
|
|
||||||
c.SetCache(key, ret)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBGttInterfaceFunc func() interface{}
|
|
||||||
|
|
||||||
// SimpleInterfaceCache 缓存
|
|
||||||
type SimpleInterfaceCache struct {
|
|
||||||
Operation *StringOperation // 操作类
|
|
||||||
Expire time.Duration // 过期时间
|
|
||||||
DBGetter DBGttInterfaceFunc // 缓存不存在的操作 DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSimpleInterfaceCache 构造函数
|
|
||||||
func (app *App) NewSimpleInterfaceCache(operation *StringOperation, expire time.Duration) *SimpleInterfaceCache {
|
|
||||||
return &SimpleInterfaceCache{
|
|
||||||
Operation: operation, // 操作类
|
|
||||||
Expire: expire, // 过期时间
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCache 设置缓存
|
|
||||||
func (c *SimpleInterfaceCache) SetCache(key string, value interface{}) {
|
|
||||||
c.Operation.Set(key, value, WithExpire(c.Expire)).Unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCache 获取缓存
|
|
||||||
func (c *SimpleInterfaceCache) GetCache(key string) (ret string) {
|
|
||||||
f := func() string {
|
|
||||||
obj := c.DBGetter()
|
|
||||||
b, err := json.Marshal(obj)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
ret = c.Operation.Get(key).UnwrapOrElse(f)
|
|
||||||
c.SetCache(key, ret)
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBGttJsonFunc func() interface{}
|
|
||||||
|
|
||||||
// SimpleJsonCache 缓存
|
|
||||||
type SimpleJsonCache struct {
|
|
||||||
Operation *StringOperation // 操作类
|
|
||||||
Expire time.Duration // 过期时间
|
|
||||||
DBGetter DBGttJsonFunc // 缓存不存在的操作 DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSimpleJsonCache 构造函数
|
|
||||||
func (app *App) NewSimpleJsonCache(operation *StringOperation, expire time.Duration) *SimpleJsonCache {
|
|
||||||
return &SimpleJsonCache{
|
|
||||||
Operation: operation, // 操作类
|
|
||||||
Expire: expire, // 过期时间
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCache 设置缓存
|
|
||||||
func (c *SimpleJsonCache) SetCache(key string, value interface{}) {
|
|
||||||
c.Operation.Set(key, value, WithExpire(c.Expire)).Unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCache 获取缓存
|
|
||||||
func (c *SimpleJsonCache) GetCache(key string) (ret interface{}) {
|
|
||||||
f := func() string {
|
|
||||||
obj := c.DBGetter()
|
|
||||||
b, err := json.Marshal(obj)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
ret = c.Operation.Get(key).UnwrapOrElse(f)
|
|
||||||
c.SetCache(key, ret)
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,37 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DBGttStringFunc func() string
|
|
||||||
|
|
||||||
// SimpleStringCache 缓存
|
|
||||||
type SimpleStringCache struct {
|
|
||||||
Operation *StringOperation // 操作类
|
|
||||||
Expire time.Duration // 过期时间
|
|
||||||
DBGetter DBGttStringFunc // 缓存不存在的操作 DB
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSimpleStringCache 构造函数
|
|
||||||
func (app *App) NewSimpleStringCache(operation *StringOperation, expire time.Duration) *SimpleStringCache {
|
|
||||||
return &SimpleStringCache{
|
|
||||||
Operation: operation, // 操作类
|
|
||||||
Expire: expire, // 过期时间
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCache 设置缓存
|
|
||||||
func (c *SimpleStringCache) SetCache(key string, value string) {
|
|
||||||
c.Operation.Set(key, value, WithExpire(c.Expire)).Unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCache 获取缓存
|
|
||||||
func (c *SimpleStringCache) GetCache(key string) (ret string) {
|
|
||||||
f := func() string {
|
|
||||||
return c.DBGetter()
|
|
||||||
}
|
|
||||||
ret = c.Operation.Get(key).UnwrapOrElse(f)
|
|
||||||
c.SetCache(key, ret)
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
type SliceResult struct {
|
|
||||||
Result []interface{}
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSliceResult 构造函数
|
|
||||||
func NewSliceResult(result []interface{}, err error) *SliceResult {
|
|
||||||
return &SliceResult{Result: result, Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap 空值情况下返回错误
|
|
||||||
func (r *SliceResult) Unwrap() []interface{} {
|
|
||||||
if r.Err != nil {
|
|
||||||
panic(r.Err)
|
|
||||||
}
|
|
||||||
return r.Result
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnwrapOr 空值情况下设置返回默认值
|
|
||||||
func (r *SliceResult) UnwrapOr(defaults []interface{}) []interface{} {
|
|
||||||
if r.Err != nil {
|
|
||||||
return defaults
|
|
||||||
}
|
|
||||||
return r.Result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *SliceResult) Iter() *Iterator {
|
|
||||||
return NewIterator(r.Result)
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StringOperation struct {
|
|
||||||
db *redis.Client
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *App) NewStringOperation() *StringOperation {
|
|
||||||
return &StringOperation{
|
|
||||||
db: app.Rdb,
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set 设置
|
|
||||||
func (o *StringOperation) Set(key string, value interface{}, attrs ...*OperationAttr) *StringResult {
|
|
||||||
exp := OperationAttrs(attrs).Find(AttrExpr)
|
|
||||||
if exp == nil {
|
|
||||||
exp = time.Second * 0
|
|
||||||
}
|
|
||||||
return NewStringResult(o.db.Set(o.ctx, key, value, exp.(time.Duration)).Result())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取单个
|
|
||||||
func (o *StringOperation) Get(key string) *StringResult {
|
|
||||||
return NewStringResult(o.db.Get(o.ctx, key).Result())
|
|
||||||
}
|
|
||||||
|
|
||||||
// MGet 获取多个
|
|
||||||
func (o *StringOperation) MGet(keys ...string) *SliceResult {
|
|
||||||
return NewSliceResult(o.db.MGet(o.ctx, keys...).Result())
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
package goredis
|
|
||||||
|
|
||||||
type StringResult struct {
|
|
||||||
Result string // 结果
|
|
||||||
Err error // 错误
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStringResult 构造函数
|
|
||||||
func NewStringResult(result string, err error) *StringResult {
|
|
||||||
return &StringResult{
|
|
||||||
Result: result,
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwrap 空值情况下返回错误
|
|
||||||
func (r *StringResult) Unwrap() string {
|
|
||||||
if r.Err != nil {
|
|
||||||
panic(r.Err)
|
|
||||||
}
|
|
||||||
return r.Result
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnwrapOr 空值情况下设置返回默认值
|
|
||||||
func (r *StringResult) UnwrapOr(defaults string) string {
|
|
||||||
if r.Err != nil {
|
|
||||||
return defaults
|
|
||||||
}
|
|
||||||
return r.Result
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnwrapOrElse 空值情况下设置返回其他
|
|
||||||
func (r *StringResult) UnwrapOrElse(f func() string) string {
|
|
||||||
if r.Err != nil {
|
|
||||||
return f()
|
|
||||||
}
|
|
||||||
return r.Result
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 茂名聚合科技有限公司
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
@ -1,117 +0,0 @@
|
|||||||
package gostring
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"unicode/utf8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ToFloat64 string到float64
|
|
||||||
func ToFloat64(s string) float64 {
|
|
||||||
i, _ := strconv.ParseFloat(s, 64)
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToInt string到int
|
|
||||||
func ToInt(s string) int {
|
|
||||||
i, _ := strconv.Atoi(s)
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToInt64 string到int64
|
|
||||||
func ToInt64(s string) int64 {
|
|
||||||
i, err := strconv.ParseInt(s, 10, 64)
|
|
||||||
if err == nil {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
return int64(ToFloat64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace 字符串替换
|
|
||||||
func Replace(str, old, new string) string {
|
|
||||||
return strings.Replace(str, old, new, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func HmacSha256Hex(key, strToSign string) string {
|
|
||||||
hasHer := hmac.New(sha256.New, []byte(key))
|
|
||||||
hasHer.Write([]byte(strToSign))
|
|
||||||
return hex.EncodeToString(hasHer.Sum(nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Space 去除空格
|
|
||||||
func Space(str string) string {
|
|
||||||
return strings.Replace(str, " ", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LineBreak 去除换行符
|
|
||||||
func LineBreak(str string) string {
|
|
||||||
return strings.Replace(str, "\n", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpaceAndLineBreak 去除空格和去除换行符
|
|
||||||
func SpaceAndLineBreak(str string) string {
|
|
||||||
return LineBreak(Space(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrimLastChar 删除字符串中的最后一个
|
|
||||||
func TrimLastChar(s string) string {
|
|
||||||
r, size := utf8.DecodeLastRuneInString(s)
|
|
||||||
if r == utf8.RuneError && (size == 0 || size == 1) {
|
|
||||||
size = 0
|
|
||||||
}
|
|
||||||
return s[:len(s)-size]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Split 字符串分隔
|
|
||||||
func Split(s string, sep string) []string {
|
|
||||||
return strings.Split(s, sep)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NumericalToString(value interface{}) (string, bool) {
|
|
||||||
var val string
|
|
||||||
|
|
||||||
switch value.(type) {
|
|
||||||
default:
|
|
||||||
return "0", false
|
|
||||||
case int:
|
|
||||||
intVal, _ := value.(int)
|
|
||||||
val = strconv.FormatInt(int64(intVal), 10)
|
|
||||||
case int8:
|
|
||||||
intVal, _ := value.(int8)
|
|
||||||
val = strconv.FormatInt(int64(intVal), 10)
|
|
||||||
case int16:
|
|
||||||
intVal, _ := value.(int16)
|
|
||||||
val = strconv.FormatInt(int64(intVal), 10)
|
|
||||||
case int32:
|
|
||||||
intVal, _ := value.(int32)
|
|
||||||
val = strconv.FormatInt(int64(intVal), 10)
|
|
||||||
case int64:
|
|
||||||
intVal, _ := value.(int64)
|
|
||||||
val = strconv.FormatInt(int64(intVal), 10)
|
|
||||||
case uint:
|
|
||||||
intVal, _ := value.(uint)
|
|
||||||
val = strconv.FormatUint(uint64(intVal), 10)
|
|
||||||
case uint8:
|
|
||||||
intVal, _ := value.(uint8)
|
|
||||||
val = strconv.FormatUint(uint64(intVal), 10)
|
|
||||||
case uint16:
|
|
||||||
intVal, _ := value.(uint16)
|
|
||||||
val = strconv.FormatUint(uint64(intVal), 10)
|
|
||||||
case uint32:
|
|
||||||
intVal, _ := value.(uint32)
|
|
||||||
val = strconv.FormatUint(uint64(intVal), 10)
|
|
||||||
case uint64:
|
|
||||||
intVal, _ := value.(uint64)
|
|
||||||
val = strconv.FormatUint(intVal, 10)
|
|
||||||
case float32:
|
|
||||||
floatVal, _ := value.(float32)
|
|
||||||
val = strconv.FormatFloat(float64(floatVal), 'f', -1, 32)
|
|
||||||
case float64:
|
|
||||||
floatVal, _ := value.(float64)
|
|
||||||
val = strconv.FormatFloat(floatVal, 'f', -1, 64)
|
|
||||||
}
|
|
||||||
return val, true
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2018 茂名聚合科技有限公司
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
@ -1,15 +0,0 @@
|
|||||||
package gotime
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// Tomorrow 明天
|
|
||||||
func Tomorrow() Pro {
|
|
||||||
p := NewPro()
|
|
||||||
location, err := time.LoadLocation("Asia/Shanghai")
|
|
||||||
if err != nil {
|
|
||||||
p.Time = time.Now().Add(time.Hour*8).AddDate(0, 0, +1)
|
|
||||||
} else {
|
|
||||||
p.Time = time.Now().In(location).AddDate(0, 0, +1)
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
package gotime
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 时间格式化常量
|
|
||||||
const (
|
|
||||||
RFC3339Format = time.RFC3339
|
|
||||||
Iso8601Format = "2006-01-02T15:04:05-07:00"
|
|
||||||
CookieFormat = "Monday, 02-Jan-2006 15:04:05 MST"
|
|
||||||
RFC1036Format = "Mon, 02 Jan 06 15:04:05 -0700"
|
|
||||||
RFC7231Format = "Mon, 02 Jan 2006 15:04:05 GMT"
|
|
||||||
DayDateTimeFormat = "Mon, Jan 2, 2006 3:04 PM"
|
|
||||||
DateTimeFormat = "2006-01-02 15:04:05"
|
|
||||||
DateFormat = "2006-01-02"
|
|
||||||
TimeFormat = "15:04:05"
|
|
||||||
ShortDateTimeFormat = "20060102150405"
|
|
||||||
ShortDateFormat = "20060102"
|
|
||||||
ShortTimeFormat = "150405"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Pro 结构体
|
|
||||||
type Pro struct {
|
|
||||||
Time time.Time
|
|
||||||
loc *time.Location
|
|
||||||
Error error
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewPro 初始化结构体
|
|
||||||
func NewPro() Pro {
|
|
||||||
return Pro{
|
|
||||||
Time: time.Now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeSeconds 获取n秒前的时间
|
|
||||||
func (p Pro) BeforeSeconds(seconds int) Pro {
|
|
||||||
st, _ := time.ParseDuration(fmt.Sprintf("-%ds", seconds))
|
|
||||||
p.Time = p.Time.Add(st)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// AfterSeconds 获取n秒后的时间
|
|
||||||
func (p Pro) AfterSeconds(seconds int) Pro {
|
|
||||||
st, _ := time.ParseDuration(fmt.Sprintf("+%ds", seconds))
|
|
||||||
p.Time = p.Time.Add(st)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeHour 获取n小时前的时间
|
|
||||||
func (p Pro) BeforeHour(hour int) Pro {
|
|
||||||
st, _ := time.ParseDuration(fmt.Sprintf("-%dh", hour))
|
|
||||||
p.Time = p.Time.Add(st)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// AfterHour 获取n小时后的时间
|
|
||||||
func (p Pro) AfterHour(hour int) Pro {
|
|
||||||
st, _ := time.ParseDuration(fmt.Sprintf("+%dh", hour))
|
|
||||||
p.Time = p.Time.Add(st)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// BeforeDay 获取n天前的时间
|
|
||||||
func (p Pro) BeforeDay(day int) Pro {
|
|
||||||
p.Time = p.Time.AddDate(0, 0, -day)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// AfterDay 获取n天后的时间
|
|
||||||
func (p Pro) AfterDay(day int) Pro {
|
|
||||||
p.Time = p.Time.AddDate(0, 0, day)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFormat 格式化
|
|
||||||
func (p Pro) SetFormat(layout string) string {
|
|
||||||
return p.Time.Format(layout)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Month 获取当前月
|
|
||||||
func (p Pro) Month() int {
|
|
||||||
return p.MonthOfYear()
|
|
||||||
}
|
|
||||||
|
|
||||||
// MonthOfYear 获取本年的第几月
|
|
||||||
func (p Pro) MonthOfYear() int {
|
|
||||||
return int(p.Time.In(p.loc).Month())
|
|
||||||
}
|
|
@ -1,69 +0,0 @@
|
|||||||
package gotime
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// DiffInHour 相差多少小时
|
|
||||||
func (p Pro) DiffInHour(t2 time.Time) (hour int64) {
|
|
||||||
t2.Before(p.Time)
|
|
||||||
diff := p.Time.Unix() - t2.Unix()
|
|
||||||
hour = diff / 3600
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffInHourWithAbs 相差多少小时(绝对值)
|
|
||||||
func (p Pro) DiffInHourWithAbs(t2 time.Time) (hour int64) {
|
|
||||||
p.Time.Before(t2)
|
|
||||||
diff := t2.Unix() - p.Time.Unix()
|
|
||||||
hour = diff / 3600
|
|
||||||
if hour > 0 {
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
t2.Before(p.Time)
|
|
||||||
diff = p.Time.Unix() - t2.Unix()
|
|
||||||
hour = diff / 3600
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffInMinutes 相差多少分钟
|
|
||||||
func (p Pro) DiffInMinutes(t2 time.Time) (hour int64) {
|
|
||||||
t2.Before(p.Time)
|
|
||||||
diff := p.Time.Unix() - t2.Unix()
|
|
||||||
hour = diff / 60
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffInMinutesWithAbs 相差多少分钟(绝对值)
|
|
||||||
func (p Pro) DiffInMinutesWithAbs(t2 time.Time) (hour int64) {
|
|
||||||
p.Time.Before(t2)
|
|
||||||
diff := t2.Unix() - p.Time.Unix()
|
|
||||||
hour = diff / 60
|
|
||||||
if hour > 0 {
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
t2.Before(p.Time)
|
|
||||||
diff = p.Time.Unix() - t2.Unix()
|
|
||||||
hour = diff / 60
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffInSecond 相差多少秒
|
|
||||||
func (p Pro) DiffInSecond(t2 time.Time) (hour int64) {
|
|
||||||
t2.Before(p.Time)
|
|
||||||
diff := p.Time.Unix() - t2.Unix()
|
|
||||||
hour = diff
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
|
|
||||||
// DiffInSecondWithAbs 相差多少秒(绝对值)
|
|
||||||
func (p Pro) DiffInSecondWithAbs(t2 time.Time) (hour int64) {
|
|
||||||
p.Time.Before(t2)
|
|
||||||
diff := t2.Unix() - p.Time.Unix()
|
|
||||||
hour = diff
|
|
||||||
if hour > 0 {
|
|
||||||
return hour
|
|
||||||
}
|
|
||||||
t2.Before(p.Time)
|
|
||||||
diff = p.Time.Unix() - t2.Unix()
|
|
||||||
hour = diff
|
|
||||||
return hour
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
package gotime
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// invalidTimezoneError 无效的时区错误
|
|
||||||
var invalidTimezoneError = func(timezone string) error {
|
|
||||||
return fmt.Errorf("invalid timezone %q, please see the file %q for all valid timezones", timezone, "$GOROOT/lib/time/zoneinfo.zip")
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
package gotime
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 通过时区获取 Location 实例
|
|
||||||
func getLocationByTimezone(timezone string) (*time.Location, error) {
|
|
||||||
loc, err := time.LoadLocation(timezone)
|
|
||||||
if err != nil {
|
|
||||||
err = invalidTimezoneError(timezone)
|
|
||||||
}
|
|
||||||
return loc, err
|
|
||||||
}
|
|
@ -1,124 +0,0 @@
|
|||||||
package gotime
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// 数字常量
|
|
||||||
const (
|
|
||||||
YearsPerMillennium = 1000 // 每千年1000年
|
|
||||||
YearsPerCentury = 100 // 每世纪100年
|
|
||||||
YearsPerDecade = 10 // 每十年10年
|
|
||||||
QuartersPerYear = 4 // 每年4季度
|
|
||||||
MonthsPerYear = 12 // 每年12月
|
|
||||||
MonthsPerQuarter = 3 // 每季度3月
|
|
||||||
WeeksPerNormalYear = 52 // 每常规年52周
|
|
||||||
weeksPerLongYear = 53 // 每长年53周
|
|
||||||
WeeksPerMonth = 4 // 每月4周
|
|
||||||
DaysPerLeapYear = 366 // 每闰年366天
|
|
||||||
DaysPerNormalYear = 365 // 每常规年365天
|
|
||||||
DaysPerWeek = 7 // 每周7天
|
|
||||||
HoursPerWeek = 168 // 每周168小时
|
|
||||||
HoursPerDay = 24 // 每天24小时
|
|
||||||
MinutesPerDay = 1440 // 每天1440分钟
|
|
||||||
MinutesPerHour = 60 // 每小时60分钟
|
|
||||||
SecondsPerWeek = 604800 // 每周604800秒
|
|
||||||
SecondsPerDay = 86400 // 每天86400秒
|
|
||||||
SecondsPerHour = 3600 // 每小时3600秒
|
|
||||||
SecondsPerMinute = 60 // 每分钟60秒
|
|
||||||
MillisecondsPerSecond = 1000 // 每秒1000毫秒
|
|
||||||
MicrosecondsPerMillisecond = 1000 // 每毫秒1000微秒
|
|
||||||
MicrosecondsPerSecond = 1000000 // 每秒1000000微秒
|
|
||||||
)
|
|
||||||
|
|
||||||
// StartOfCentury 本世纪开始时间
|
|
||||||
func (p Pro) StartOfCentury() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year()/YearsPerCentury*YearsPerCentury, 1, 1, 0, 0, 0, 0, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndOfCentury 本世纪结束时间
|
|
||||||
func (p Pro) EndOfCentury() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year()/YearsPerCentury*YearsPerCentury+99, 12, 31, 23, 59, 59, 999999999, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartOfDecade 本年代开始时间
|
|
||||||
func (p Pro) StartOfDecade() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year()/YearsPerDecade*YearsPerDecade, 1, 1, 0, 0, 0, 0, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndOfDecade 本年代结束时间
|
|
||||||
func (p Pro) EndOfDecade() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year()/YearsPerDecade*YearsPerDecade+9, 12, 31, 23, 59, 59, 999999999, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartOfYear 本年开始时间
|
|
||||||
func (p Pro) StartOfYear() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year(), 1, 1, 0, 0, 0, 0, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndOfYear 本年结束时间
|
|
||||||
func (p Pro) EndOfYear() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year(), 12, 31, 23, 59, 59, 999999999, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quarter 获取当前季度
|
|
||||||
func (p Pro) Quarter() (quarter int) {
|
|
||||||
switch {
|
|
||||||
case p.Time.Month() >= 10:
|
|
||||||
quarter = 4
|
|
||||||
case p.Time.Month() >= 7:
|
|
||||||
quarter = 3
|
|
||||||
case p.Time.Month() >= 4:
|
|
||||||
quarter = 2
|
|
||||||
case p.Time.Month() >= 1:
|
|
||||||
quarter = 1
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartOfQuarter 本季度开始时间
|
|
||||||
func (p Pro) StartOfQuarter() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year(), time.Month(3*p.Quarter()-2), 1, 0, 0, 0, 0, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndOfQuarter 本季度结束时间
|
|
||||||
func (p Pro) EndOfQuarter() Pro {
|
|
||||||
quarter, day := p.Quarter(), 30
|
|
||||||
switch quarter {
|
|
||||||
case 1, 4:
|
|
||||||
day = 31
|
|
||||||
case 2, 3:
|
|
||||||
day = 30
|
|
||||||
}
|
|
||||||
p.Time = time.Date(p.Time.Year(), time.Month(3*quarter), day, 23, 59, 59, 999999999, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartOfMonth 本月开始时间
|
|
||||||
func (p Pro) StartOfMonth() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year(), time.Month(p.Month()), 1, 0, 0, 0, 0, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndOfMonth 本月结束时间
|
|
||||||
func (p Pro) EndOfMonth() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year(), time.Month(p.Month()), 1, 23, 59, 59, 999999999, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// StartOfDay 本日开始时间
|
|
||||||
func (p Pro) StartOfDay() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year(), p.Time.Month(), p.Time.Day(), 0, 0, 0, 0, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
// EndOfDay 本日结束时间
|
|
||||||
func (p Pro) EndOfDay() Pro {
|
|
||||||
p.Time = time.Date(p.Time.Year(), p.Time.Month(), p.Time.Day(), 23, 59, 59, 0, p.Time.Location())
|
|
||||||
return p
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
package gotime
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Yesterday 昨天
|
|
||||||
func Yesterday() Pro {
|
|
||||||
p := NewPro()
|
|
||||||
location, err := time.LoadLocation("Asia/Shanghai")
|
|
||||||
if err != nil {
|
|
||||||
p.Time = time.Now().Add(time.Hour*8).AddDate(0, 0, -1)
|
|
||||||
} else {
|
|
||||||
p.Time = time.Now().In(location).AddDate(0, 0, -1)
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
*.rdb
|
|
||||||
testdata/*/
|
|
||||||
.idea/
|
|
@ -1,4 +0,0 @@
|
|||||||
run:
|
|
||||||
concurrency: 8
|
|
||||||
deadline: 5m
|
|
||||||
tests: false
|
|
@ -1,4 +0,0 @@
|
|||||||
semi: false
|
|
||||||
singleQuote: true
|
|
||||||
proseWrap: always
|
|
||||||
printWidth: 100
|
|
@ -1,177 +0,0 @@
|
|||||||
## [8.11.5](https://github.com/go-redis/redis/compare/v8.11.4...v8.11.5) (2022-03-17)
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes
|
|
||||||
|
|
||||||
* add missing Expire methods to Cmdable ([17e3b43](https://github.com/go-redis/redis/commit/17e3b43879d516437ada71cf9c0deac6a382ed9a))
|
|
||||||
* add whitespace for avoid unlikely colisions ([7f7c181](https://github.com/go-redis/redis/commit/7f7c1817617cfec909efb13d14ad22ef05a6ad4c))
|
|
||||||
* example/otel compile error ([#2028](https://github.com/go-redis/redis/issues/2028)) ([187c07c](https://github.com/go-redis/redis/commit/187c07c41bf68dc3ab280bc3a925e960bbef6475))
|
|
||||||
* **extra/redisotel:** set span.kind attribute to client ([065b200](https://github.com/go-redis/redis/commit/065b200070b41e6e949710b4f9e01b50ccc60ab2))
|
|
||||||
* format ([96f53a0](https://github.com/go-redis/redis/commit/96f53a0159a28affa94beec1543a62234e7f8b32))
|
|
||||||
* invalid type assert in stringArg ([de6c131](https://github.com/go-redis/redis/commit/de6c131865b8263400c8491777b295035f2408e4))
|
|
||||||
* rename Golang to Go ([#2030](https://github.com/go-redis/redis/issues/2030)) ([b82a2d9](https://github.com/go-redis/redis/commit/b82a2d9d4d2de7b7cbe8fcd4895be62dbcacacbc))
|
|
||||||
* set timeout for WAIT command. Fixes [#1963](https://github.com/go-redis/redis/issues/1963) ([333fee1](https://github.com/go-redis/redis/commit/333fee1a8fd98a2fbff1ab187c1b03246a7eb01f))
|
|
||||||
* update some argument counts in pre-allocs ([f6974eb](https://github.com/go-redis/redis/commit/f6974ebb5c40a8adf90d2cacab6dc297f4eba4c2))
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Add redis v7's NX, XX, GT, LT expire variants ([e19bbb2](https://github.com/go-redis/redis/commit/e19bbb26e2e395c6e077b48d80d79e99f729a8b8))
|
|
||||||
* add support for acl sentinel auth in universal client ([ab0ccc4](https://github.com/go-redis/redis/commit/ab0ccc47413f9b2a6eabc852fed5005a3ee1af6e))
|
|
||||||
* add support for COPY command ([#2016](https://github.com/go-redis/redis/issues/2016)) ([730afbc](https://github.com/go-redis/redis/commit/730afbcffb93760e8a36cc06cfe55ab102b693a7))
|
|
||||||
* add support for passing extra attributes added to spans ([39faaa1](https://github.com/go-redis/redis/commit/39faaa171523834ba527c9789710c4fde87f5a2e))
|
|
||||||
* add support for time.Duration write and scan ([2f1b74e](https://github.com/go-redis/redis/commit/2f1b74e20cdd7719b2aecf0768d3e3ae7c3e781b))
|
|
||||||
* **redisotel:** ability to override TracerProvider ([#1998](https://github.com/go-redis/redis/issues/1998)) ([bf8d4aa](https://github.com/go-redis/redis/commit/bf8d4aa60c00366cda2e98c3ddddc8cf68507417))
|
|
||||||
* set net.peer.name and net.peer.port in otel example ([69bf454](https://github.com/go-redis/redis/commit/69bf454f706204211cd34835f76b2e8192d3766d))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [8.11.4](https://github.com/go-redis/redis/compare/v8.11.3...v8.11.4) (2021-10-04)
|
|
||||||
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* add acl auth support for sentinels ([f66582f](https://github.com/go-redis/redis/commit/f66582f44f3dc3a4705a5260f982043fde4aa634))
|
|
||||||
* add Cmd.{String,Int,Float,Bool}Slice helpers and an example ([5d3d293](https://github.com/go-redis/redis/commit/5d3d293cc9c60b90871e2420602001463708ce24))
|
|
||||||
* add SetVal method for each command ([168981d](https://github.com/go-redis/redis/commit/168981da2d84ee9e07d15d3e74d738c162e264c4))
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## v8.11
|
|
||||||
|
|
||||||
- Remove OpenTelemetry metrics.
|
|
||||||
- Supports more redis commands and options.
|
|
||||||
|
|
||||||
## v8.10
|
|
||||||
|
|
||||||
- Removed extra OpenTelemetry spans from go-redis core. Now go-redis instrumentation only adds a
|
|
||||||
single span with a Redis command (instead of 4 spans). There are multiple reasons behind this
|
|
||||||
decision:
|
|
||||||
|
|
||||||
- Traces become smaller and less noisy.
|
|
||||||
- It may be costly to process those 3 extra spans for each query.
|
|
||||||
- go-redis no longer depends on OpenTelemetry.
|
|
||||||
|
|
||||||
Eventually we hope to replace the information that we no longer collect with OpenTelemetry
|
|
||||||
Metrics.
|
|
||||||
|
|
||||||
## v8.9
|
|
||||||
|
|
||||||
- Changed `PubSub.Channel` to only rely on `Ping` result. You can now use `WithChannelSize`,
|
|
||||||
`WithChannelHealthCheckInterval`, and `WithChannelSendTimeout` to override default settings.
|
|
||||||
|
|
||||||
## v8.8
|
|
||||||
|
|
||||||
- To make updating easier, extra modules now have the same version as go-redis does. That means that
|
|
||||||
you need to update your imports:
|
|
||||||
|
|
||||||
```
|
|
||||||
github.com/go-redis/redis/extra/redisotel -> github.com/go-redis/redis/extra/redisotel/v8
|
|
||||||
github.com/go-redis/redis/extra/rediscensus -> github.com/go-redis/redis/extra/rediscensus/v8
|
|
||||||
```
|
|
||||||
|
|
||||||
## v8.5
|
|
||||||
|
|
||||||
- [knadh](https://github.com/knadh) contributed long-awaited ability to scan Redis Hash into a
|
|
||||||
struct:
|
|
||||||
|
|
||||||
```go
|
|
||||||
err := rdb.HGetAll(ctx, "hash").Scan(&data)
|
|
||||||
|
|
||||||
err := rdb.MGet(ctx, "key1", "key2").Scan(&data)
|
|
||||||
```
|
|
||||||
|
|
||||||
- Please check [redismock](https://github.com/go-redis/redismock) by
|
|
||||||
[monkey92t](https://github.com/monkey92t) if you are looking for mocking Redis Client.
|
|
||||||
|
|
||||||
## v8
|
|
||||||
|
|
||||||
- All commands require `context.Context` as a first argument, e.g. `rdb.Ping(ctx)`. If you are not
|
|
||||||
using `context.Context` yet, the simplest option is to define global package variable
|
|
||||||
`var ctx = context.TODO()` and use it when `ctx` is required.
|
|
||||||
|
|
||||||
- Full support for `context.Context` canceling.
|
|
||||||
|
|
||||||
- Added `redis.NewFailoverClusterClient` that supports routing read-only commands to a slave node.
|
|
||||||
|
|
||||||
- Added `redisext.OpenTemetryHook` that adds
|
|
||||||
[Redis OpenTelemetry instrumentation](https://redis.uptrace.dev/tracing/).
|
|
||||||
|
|
||||||
- Redis slow log support.
|
|
||||||
|
|
||||||
- Ring uses Rendezvous Hashing by default which provides better distribution. You need to move
|
|
||||||
existing keys to a new location or keys will be inaccessible / lost. To use old hashing scheme:
|
|
||||||
|
|
||||||
```go
|
|
||||||
import "github.com/golang/groupcache/consistenthash"
|
|
||||||
|
|
||||||
ring := redis.NewRing(&redis.RingOptions{
|
|
||||||
NewConsistentHash: func() {
|
|
||||||
return consistenthash.New(100, crc32.ChecksumIEEE)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
- `ClusterOptions.MaxRedirects` default value is changed from 8 to 3.
|
|
||||||
- `Options.MaxRetries` default value is changed from 0 to 3.
|
|
||||||
|
|
||||||
- `Cluster.ForEachNode` is renamed to `ForEachShard` for consistency with `Ring`.
|
|
||||||
|
|
||||||
## v7.3
|
|
||||||
|
|
||||||
- New option `Options.Username` which causes client to use `AuthACL`. Be aware if your connection
|
|
||||||
URL contains username.
|
|
||||||
|
|
||||||
## v7.2
|
|
||||||
|
|
||||||
- Existing `HMSet` is renamed to `HSet` and old deprecated `HMSet` is restored for Redis 3 users.
|
|
||||||
|
|
||||||
## v7.1
|
|
||||||
|
|
||||||
- Existing `Cmd.String` is renamed to `Cmd.Text`. New `Cmd.String` implements `fmt.Stringer`
|
|
||||||
interface.
|
|
||||||
|
|
||||||
## v7
|
|
||||||
|
|
||||||
- _Important_. Tx.Pipeline now returns a non-transactional pipeline. Use Tx.TxPipeline for a
|
|
||||||
transactional pipeline.
|
|
||||||
- WrapProcess is replaced with more convenient AddHook that has access to context.Context.
|
|
||||||
- WithContext now can not be used to create a shallow copy of the client.
|
|
||||||
- New methods ProcessContext, DoContext, and ExecContext.
|
|
||||||
- Client respects Context.Deadline when setting net.Conn deadline.
|
|
||||||
- Client listens on Context.Done while waiting for a connection from the pool and returns an error
|
|
||||||
when context context is cancelled.
|
|
||||||
- Add PubSub.ChannelWithSubscriptions that sends `*Subscription` in addition to `*Message` to allow
|
|
||||||
detecting reconnections.
|
|
||||||
- `time.Time` is now marshalled in RFC3339 format. `rdb.Get("foo").Time()` helper is added to parse
|
|
||||||
the time.
|
|
||||||
- `SetLimiter` is removed and added `Options.Limiter` instead.
|
|
||||||
- `HMSet` is deprecated as of Redis v4.
|
|
||||||
|
|
||||||
## v6.15
|
|
||||||
|
|
||||||
- Cluster and Ring pipelines process commands for each node in its own goroutine.
|
|
||||||
|
|
||||||
## 6.14
|
|
||||||
|
|
||||||
- Added Options.MinIdleConns.
|
|
||||||
- Added Options.MaxConnAge.
|
|
||||||
- PoolStats.FreeConns is renamed to PoolStats.IdleConns.
|
|
||||||
- Add Client.Do to simplify creating custom commands.
|
|
||||||
- Add Cmd.String, Cmd.Int, Cmd.Int64, Cmd.Uint64, Cmd.Float64, and Cmd.Bool helpers.
|
|
||||||
- Lower memory usage.
|
|
||||||
|
|
||||||
## v6.13
|
|
||||||
|
|
||||||
- Ring got new options called `HashReplicas` and `Hash`. It is recommended to set
|
|
||||||
`HashReplicas = 1000` for better keys distribution between shards.
|
|
||||||
- Cluster client was optimized to use much less memory when reloading cluster state.
|
|
||||||
- PubSub.ReceiveMessage is re-worked to not use ReceiveTimeout so it does not lose data when timeout
|
|
||||||
occurres. In most cases it is recommended to use PubSub.Channel instead.
|
|
||||||
- Dialer.KeepAlive is set to 5 minutes by default.
|
|
||||||
|
|
||||||
## v6.12
|
|
||||||
|
|
||||||
- ClusterClient got new option called `ClusterSlots` which allows to build cluster of normal Redis
|
|
||||||
Servers that don't have cluster mode enabled. See
|
|
||||||
https://godoc.org/github.com/go-redis/redis#example-NewClusterClient--ManualSetup
|
|
@ -1,25 +0,0 @@
|
|||||||
Copyright (c) 2013 The github.com/go-redis/redis Authors.
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,35 +0,0 @@
|
|||||||
PACKAGE_DIRS := $(shell find . -mindepth 2 -type f -name 'go.mod' -exec dirname {} \; | sort)
|
|
||||||
|
|
||||||
test: testdeps
|
|
||||||
go test ./...
|
|
||||||
go test ./... -short -race
|
|
||||||
go test ./... -run=NONE -bench=. -benchmem
|
|
||||||
env GOOS=linux GOARCH=386 go test ./...
|
|
||||||
go vet
|
|
||||||
|
|
||||||
testdeps: testdata/redis/src/redis-server
|
|
||||||
|
|
||||||
bench: testdeps
|
|
||||||
go test ./... -test.run=NONE -test.bench=. -test.benchmem
|
|
||||||
|
|
||||||
.PHONY: all test testdeps bench
|
|
||||||
|
|
||||||
testdata/redis:
|
|
||||||
mkdir -p $@
|
|
||||||
wget -qO- https://download.redis.io/releases/redis-6.2.5.tar.gz | tar xvz --strip-components=1 -C $@
|
|
||||||
|
|
||||||
testdata/redis/src/redis-server: testdata/redis
|
|
||||||
cd $< && make all
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
gofmt -w -s ./
|
|
||||||
goimports -w -local github.com/go-redis/redis ./
|
|
||||||
|
|
||||||
go_mod_tidy:
|
|
||||||
go get -u && go mod tidy
|
|
||||||
set -e; for dir in $(PACKAGE_DIRS); do \
|
|
||||||
echo "go mod tidy in $${dir}"; \
|
|
||||||
(cd "$${dir}" && \
|
|
||||||
go get -u && \
|
|
||||||
go mod tidy); \
|
|
||||||
done
|
|
@ -1,175 +0,0 @@
|
|||||||
# Redis client for Go
|
|
||||||
|
|
||||||
![build workflow](https://github.com/go-redis/redis/actions/workflows/build.yml/badge.svg)
|
|
||||||
[![PkgGoDev](https://pkg.go.dev/badge/github.com/go-redis/redis/v8)](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc)
|
|
||||||
[![Documentation](https://img.shields.io/badge/redis-documentation-informational)](https://redis.uptrace.dev/)
|
|
||||||
|
|
||||||
go-redis is brought to you by :star: [**uptrace/uptrace**](https://github.com/uptrace/uptrace).
|
|
||||||
Uptrace is an open source and blazingly fast **distributed tracing** backend powered by
|
|
||||||
OpenTelemetry and ClickHouse. Give it a star as well!
|
|
||||||
|
|
||||||
## Resources
|
|
||||||
|
|
||||||
- [Discussions](https://github.com/go-redis/redis/discussions)
|
|
||||||
- [Documentation](https://redis.uptrace.dev)
|
|
||||||
- [Reference](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc)
|
|
||||||
- [Examples](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#pkg-examples)
|
|
||||||
- [RealWorld example app](https://github.com/uptrace/go-treemux-realworld-example-app)
|
|
||||||
|
|
||||||
Other projects you may like:
|
|
||||||
|
|
||||||
- [Bun](https://bun.uptrace.dev) - fast and simple SQL client for PostgreSQL, MySQL, and SQLite.
|
|
||||||
- [BunRouter](https://bunrouter.uptrace.dev/) - fast and flexible HTTP router for Go.
|
|
||||||
|
|
||||||
## Ecosystem
|
|
||||||
|
|
||||||
- [Redis Mock](https://github.com/go-redis/redismock)
|
|
||||||
- [Distributed Locks](https://github.com/bsm/redislock)
|
|
||||||
- [Redis Cache](https://github.com/go-redis/cache)
|
|
||||||
- [Rate limiting](https://github.com/go-redis/redis_rate)
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- Redis 3 commands except QUIT, MONITOR, and SYNC.
|
|
||||||
- Automatic connection pooling with
|
|
||||||
[circuit breaker](https://en.wikipedia.org/wiki/Circuit_breaker_design_pattern) support.
|
|
||||||
- [Pub/Sub](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#PubSub).
|
|
||||||
- [Transactions](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client-TxPipeline).
|
|
||||||
- [Pipeline](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client.Pipeline) and
|
|
||||||
[TxPipeline](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-Client.TxPipeline).
|
|
||||||
- [Scripting](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#Script).
|
|
||||||
- [Timeouts](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#Options).
|
|
||||||
- [Redis Sentinel](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewFailoverClient).
|
|
||||||
- [Redis Cluster](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewClusterClient).
|
|
||||||
- [Cluster of Redis Servers](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-NewClusterClient-ManualSetup)
|
|
||||||
without using cluster mode and Redis Sentinel.
|
|
||||||
- [Ring](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#NewRing).
|
|
||||||
- [Instrumentation](https://pkg.go.dev/github.com/go-redis/redis/v8?tab=doc#example-package-Instrumentation).
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
go-redis supports 2 last Go versions and requires a Go version with
|
|
||||||
[modules](https://github.com/golang/go/wiki/Modules) support. So make sure to initialize a Go
|
|
||||||
module:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
go mod init github.com/my/repo
|
|
||||||
```
|
|
||||||
|
|
||||||
And then install go-redis/v8 (note _v8_ in the import; omitting it is a popular mistake):
|
|
||||||
|
|
||||||
```shell
|
|
||||||
go get github.com/go-redis/redis/v8
|
|
||||||
```
|
|
||||||
|
|
||||||
## Quickstart
|
|
||||||
|
|
||||||
```go
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
var ctx = context.Background()
|
|
||||||
|
|
||||||
func ExampleClient() {
|
|
||||||
rdb := redis.NewClient(&redis.Options{
|
|
||||||
Addr: "localhost:6379",
|
|
||||||
Password: "", // no password set
|
|
||||||
DB: 0, // use default DB
|
|
||||||
})
|
|
||||||
|
|
||||||
err := rdb.Set(ctx, "key", "value", 0).Err()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
val, err := rdb.Get(ctx, "key").Result()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Println("key", val)
|
|
||||||
|
|
||||||
val2, err := rdb.Get(ctx, "key2").Result()
|
|
||||||
if err == redis.Nil {
|
|
||||||
fmt.Println("key2 does not exist")
|
|
||||||
} else if err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
fmt.Println("key2", val2)
|
|
||||||
}
|
|
||||||
// Output: key value
|
|
||||||
// key2 does not exist
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Look and feel
|
|
||||||
|
|
||||||
Some corner cases:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// SET key value EX 10 NX
|
|
||||||
set, err := rdb.SetNX(ctx, "key", "value", 10*time.Second).Result()
|
|
||||||
|
|
||||||
// SET key value keepttl NX
|
|
||||||
set, err := rdb.SetNX(ctx, "key", "value", redis.KeepTTL).Result()
|
|
||||||
|
|
||||||
// SORT list LIMIT 0 2 ASC
|
|
||||||
vals, err := rdb.Sort(ctx, "list", &redis.Sort{Offset: 0, Count: 2, Order: "ASC"}).Result()
|
|
||||||
|
|
||||||
// ZRANGEBYSCORE zset -inf +inf WITHSCORES LIMIT 0 2
|
|
||||||
vals, err := rdb.ZRangeByScoreWithScores(ctx, "zset", &redis.ZRangeBy{
|
|
||||||
Min: "-inf",
|
|
||||||
Max: "+inf",
|
|
||||||
Offset: 0,
|
|
||||||
Count: 2,
|
|
||||||
}).Result()
|
|
||||||
|
|
||||||
// ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3 AGGREGATE SUM
|
|
||||||
vals, err := rdb.ZInterStore(ctx, "out", &redis.ZStore{
|
|
||||||
Keys: []string{"zset1", "zset2"},
|
|
||||||
Weights: []int64{2, 3}
|
|
||||||
}).Result()
|
|
||||||
|
|
||||||
// EVAL "return {KEYS[1],ARGV[1]}" 1 "key" "hello"
|
|
||||||
vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello").Result()
|
|
||||||
|
|
||||||
// custom command
|
|
||||||
res, err := rdb.Do(ctx, "set", "key", "value").Result()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Run the test
|
|
||||||
|
|
||||||
go-redis will start a redis-server and run the test cases.
|
|
||||||
|
|
||||||
The paths of redis-server bin file and redis config file are defined in `main_test.go`:
|
|
||||||
|
|
||||||
```
|
|
||||||
var (
|
|
||||||
redisServerBin, _ = filepath.Abs(filepath.Join("testdata", "redis", "src", "redis-server"))
|
|
||||||
redisServerConf, _ = filepath.Abs(filepath.Join("testdata", "redis", "redis.conf"))
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
For local testing, you can change the variables to refer to your local files, or create a soft link
|
|
||||||
to the corresponding folder for redis-server and copy the config file to `testdata/redis/`:
|
|
||||||
|
|
||||||
```
|
|
||||||
ln -s /usr/bin/redis-server ./go-redis/testdata/redis/src
|
|
||||||
cp ./go-redis/testdata/redis.conf ./go-redis/testdata/redis/
|
|
||||||
```
|
|
||||||
|
|
||||||
Lastly, run:
|
|
||||||
|
|
||||||
```
|
|
||||||
go test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Contributors
|
|
||||||
|
|
||||||
Thanks to all the people who already contributed!
|
|
||||||
|
|
||||||
<a href="https://github.com/go-redis/redis/graphs/contributors">
|
|
||||||
<img src="https://contributors-img.web.app/image?repo=go-redis/redis" />
|
|
||||||
</a>
|
|
@ -1,15 +0,0 @@
|
|||||||
# Releasing
|
|
||||||
|
|
||||||
1. Run `release.sh` script which updates versions in go.mod files and pushes a new branch to GitHub:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
TAG=v1.0.0 ./scripts/release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Open a pull request and wait for the build to finish.
|
|
||||||
|
|
||||||
3. Merge the pull request and run `tag.sh` to create tags for packages:
|
|
||||||
|
|
||||||
```shell
|
|
||||||
TAG=v1.0.0 ./scripts/tag.sh
|
|
||||||
```
|
|
File diff suppressed because it is too large
Load Diff
@ -1,109 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (c *ClusterClient) DBSize(ctx context.Context) *IntCmd {
|
|
||||||
cmd := NewIntCmd(ctx, "dbsize")
|
|
||||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
|
||||||
var size int64
|
|
||||||
err := c.ForEachMaster(ctx, func(ctx context.Context, master *Client) error {
|
|
||||||
n, err := master.DBSize(ctx).Result()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
atomic.AddInt64(&size, n)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
cmd.SetErr(err)
|
|
||||||
} else {
|
|
||||||
cmd.val = size
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClusterClient) ScriptLoad(ctx context.Context, script string) *StringCmd {
|
|
||||||
cmd := NewStringCmd(ctx, "script", "load", script)
|
|
||||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
|
||||||
mu := &sync.Mutex{}
|
|
||||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
|
||||||
val, err := shard.ScriptLoad(ctx, script).Result()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
if cmd.Val() == "" {
|
|
||||||
cmd.val = val
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
cmd.SetErr(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClusterClient) ScriptFlush(ctx context.Context) *StatusCmd {
|
|
||||||
cmd := NewStatusCmd(ctx, "script", "flush")
|
|
||||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
|
||||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
|
||||||
return shard.ScriptFlush(ctx).Err()
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
cmd.SetErr(err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ClusterClient) ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd {
|
|
||||||
args := make([]interface{}, 2+len(hashes))
|
|
||||||
args[0] = "script"
|
|
||||||
args[1] = "exists"
|
|
||||||
for i, hash := range hashes {
|
|
||||||
args[2+i] = hash
|
|
||||||
}
|
|
||||||
cmd := NewBoolSliceCmd(ctx, args...)
|
|
||||||
|
|
||||||
result := make([]bool, len(hashes))
|
|
||||||
for i := range result {
|
|
||||||
result[i] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = c.hooks.process(ctx, cmd, func(ctx context.Context, _ Cmder) error {
|
|
||||||
mu := &sync.Mutex{}
|
|
||||||
err := c.ForEachShard(ctx, func(ctx context.Context, shard *Client) error {
|
|
||||||
val, err := shard.ScriptExists(ctx, hashes...).Result()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
for i, v := range val {
|
|
||||||
result[i] = result[i] && v
|
|
||||||
}
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
cmd.SetErr(err)
|
|
||||||
} else {
|
|
||||||
cmd.val = result
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return cmd
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,4 +0,0 @@
|
|||||||
/*
|
|
||||||
Package redis implements a Redis client.
|
|
||||||
*/
|
|
||||||
package redis
|
|
@ -1,144 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrClosed performs any operation on the closed client will return this error.
|
|
||||||
var ErrClosed = pool.ErrClosed
|
|
||||||
|
|
||||||
type Error interface {
|
|
||||||
error
|
|
||||||
|
|
||||||
// RedisError is a no-op function but
|
|
||||||
// serves to distinguish types that are Redis
|
|
||||||
// errors from ordinary errors: a type is a
|
|
||||||
// Redis error if it has a RedisError method.
|
|
||||||
RedisError()
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Error = proto.RedisError("")
|
|
||||||
|
|
||||||
func shouldRetry(err error, retryTimeout bool) bool {
|
|
||||||
switch err {
|
|
||||||
case io.EOF, io.ErrUnexpectedEOF:
|
|
||||||
return true
|
|
||||||
case nil, context.Canceled, context.DeadlineExceeded:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if v, ok := err.(timeoutError); ok {
|
|
||||||
if v.Timeout() {
|
|
||||||
return retryTimeout
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
s := err.Error()
|
|
||||||
if s == "ERR max number of clients reached" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(s, "LOADING ") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(s, "READONLY ") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(s, "CLUSTERDOWN ") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(s, "TRYAGAIN ") {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isRedisError(err error) bool {
|
|
||||||
_, ok := err.(proto.RedisError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func isBadConn(err error, allowTimeout bool, addr string) bool {
|
|
||||||
switch err {
|
|
||||||
case nil:
|
|
||||||
return false
|
|
||||||
case context.Canceled, context.DeadlineExceeded:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if isRedisError(err) {
|
|
||||||
switch {
|
|
||||||
case isReadOnlyError(err):
|
|
||||||
// Close connections in read only state in case domain addr is used
|
|
||||||
// and domain resolves to a different Redis Server. See #790.
|
|
||||||
return true
|
|
||||||
case isMovedSameConnAddr(err, addr):
|
|
||||||
// Close connections when we are asked to move to the same addr
|
|
||||||
// of the connection. Force a DNS resolution when all connections
|
|
||||||
// of the pool are recycled
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if allowTimeout {
|
|
||||||
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
|
|
||||||
return !netErr.Temporary()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMovedError(err error) (moved bool, ask bool, addr string) {
|
|
||||||
if !isRedisError(err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s := err.Error()
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(s, "MOVED "):
|
|
||||||
moved = true
|
|
||||||
case strings.HasPrefix(s, "ASK "):
|
|
||||||
ask = true
|
|
||||||
default:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ind := strings.LastIndex(s, " ")
|
|
||||||
if ind == -1 {
|
|
||||||
return false, false, ""
|
|
||||||
}
|
|
||||||
addr = s[ind+1:]
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLoadingError(err error) bool {
|
|
||||||
return strings.HasPrefix(err.Error(), "LOADING ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isReadOnlyError(err error) bool {
|
|
||||||
return strings.HasPrefix(err.Error(), "READONLY ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isMovedSameConnAddr(err error, addr string) bool {
|
|
||||||
redisError := err.Error()
|
|
||||||
if !strings.HasPrefix(redisError, "MOVED ") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return strings.HasSuffix(redisError, " "+addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type timeoutError interface {
|
|
||||||
Timeout() bool
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func AppendArg(b []byte, v interface{}) []byte {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case nil:
|
|
||||||
return append(b, "<nil>"...)
|
|
||||||
case string:
|
|
||||||
return appendUTF8String(b, Bytes(v))
|
|
||||||
case []byte:
|
|
||||||
return appendUTF8String(b, v)
|
|
||||||
case int:
|
|
||||||
return strconv.AppendInt(b, int64(v), 10)
|
|
||||||
case int8:
|
|
||||||
return strconv.AppendInt(b, int64(v), 10)
|
|
||||||
case int16:
|
|
||||||
return strconv.AppendInt(b, int64(v), 10)
|
|
||||||
case int32:
|
|
||||||
return strconv.AppendInt(b, int64(v), 10)
|
|
||||||
case int64:
|
|
||||||
return strconv.AppendInt(b, v, 10)
|
|
||||||
case uint:
|
|
||||||
return strconv.AppendUint(b, uint64(v), 10)
|
|
||||||
case uint8:
|
|
||||||
return strconv.AppendUint(b, uint64(v), 10)
|
|
||||||
case uint16:
|
|
||||||
return strconv.AppendUint(b, uint64(v), 10)
|
|
||||||
case uint32:
|
|
||||||
return strconv.AppendUint(b, uint64(v), 10)
|
|
||||||
case uint64:
|
|
||||||
return strconv.AppendUint(b, v, 10)
|
|
||||||
case float32:
|
|
||||||
return strconv.AppendFloat(b, float64(v), 'f', -1, 64)
|
|
||||||
case float64:
|
|
||||||
return strconv.AppendFloat(b, v, 'f', -1, 64)
|
|
||||||
case bool:
|
|
||||||
if v {
|
|
||||||
return append(b, "true"...)
|
|
||||||
}
|
|
||||||
return append(b, "false"...)
|
|
||||||
case time.Time:
|
|
||||||
return v.AppendFormat(b, time.RFC3339Nano)
|
|
||||||
default:
|
|
||||||
return append(b, fmt.Sprint(v)...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func appendUTF8String(dst []byte, src []byte) []byte {
|
|
||||||
dst = append(dst, src...)
|
|
||||||
return dst
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
package hashtag
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
const slotNumber = 16384
|
|
||||||
|
|
||||||
// CRC16 implementation according to CCITT standards.
|
|
||||||
// Copyright 2001-2010 Georges Menie (www.menie.org)
|
|
||||||
// Copyright 2013 The Go Authors. All rights reserved.
|
|
||||||
// http://redis.io/topics/cluster-spec#appendix-a-crc16-reference-implementation-in-ansi-c
|
|
||||||
var crc16tab = [256]uint16{
|
|
||||||
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
|
||||||
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
|
||||||
0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6,
|
|
||||||
0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de,
|
|
||||||
0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485,
|
|
||||||
0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d,
|
|
||||||
0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4,
|
|
||||||
0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc,
|
|
||||||
0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823,
|
|
||||||
0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b,
|
|
||||||
0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12,
|
|
||||||
0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a,
|
|
||||||
0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41,
|
|
||||||
0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49,
|
|
||||||
0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70,
|
|
||||||
0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78,
|
|
||||||
0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f,
|
|
||||||
0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067,
|
|
||||||
0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e,
|
|
||||||
0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256,
|
|
||||||
0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d,
|
|
||||||
0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405,
|
|
||||||
0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c,
|
|
||||||
0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634,
|
|
||||||
0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab,
|
|
||||||
0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3,
|
|
||||||
0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a,
|
|
||||||
0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92,
|
|
||||||
0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9,
|
|
||||||
0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1,
|
|
||||||
0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8,
|
|
||||||
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0,
|
|
||||||
}
|
|
||||||
|
|
||||||
func Key(key string) string {
|
|
||||||
if s := strings.IndexByte(key, '{'); s > -1 {
|
|
||||||
if e := strings.IndexByte(key[s+1:], '}'); e > 0 {
|
|
||||||
return key[s+1 : s+e+1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
|
|
||||||
func RandomSlot() int {
|
|
||||||
return rand.Intn(slotNumber)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slot returns a consistent slot number between 0 and 16383
|
|
||||||
// for any given string key.
|
|
||||||
func Slot(key string) int {
|
|
||||||
if key == "" {
|
|
||||||
return RandomSlot()
|
|
||||||
}
|
|
||||||
key = Key(key)
|
|
||||||
return int(crc16sum(key)) % slotNumber
|
|
||||||
}
|
|
||||||
|
|
||||||
func crc16sum(key string) (crc uint16) {
|
|
||||||
for i := 0; i < len(key); i++ {
|
|
||||||
crc = (crc << 8) ^ crc16tab[(byte(crc>>8)^key[i])&0x00ff]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
package hscan
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// decoderFunc represents decoding functions for default built-in types.
|
|
||||||
type decoderFunc func(reflect.Value, string) error
|
|
||||||
|
|
||||||
var (
|
|
||||||
// List of built-in decoders indexed by their numeric constant values (eg: reflect.Bool = 1).
|
|
||||||
decoders = []decoderFunc{
|
|
||||||
reflect.Bool: decodeBool,
|
|
||||||
reflect.Int: decodeInt,
|
|
||||||
reflect.Int8: decodeInt8,
|
|
||||||
reflect.Int16: decodeInt16,
|
|
||||||
reflect.Int32: decodeInt32,
|
|
||||||
reflect.Int64: decodeInt64,
|
|
||||||
reflect.Uint: decodeUint,
|
|
||||||
reflect.Uint8: decodeUint8,
|
|
||||||
reflect.Uint16: decodeUint16,
|
|
||||||
reflect.Uint32: decodeUint32,
|
|
||||||
reflect.Uint64: decodeUint64,
|
|
||||||
reflect.Float32: decodeFloat32,
|
|
||||||
reflect.Float64: decodeFloat64,
|
|
||||||
reflect.Complex64: decodeUnsupported,
|
|
||||||
reflect.Complex128: decodeUnsupported,
|
|
||||||
reflect.Array: decodeUnsupported,
|
|
||||||
reflect.Chan: decodeUnsupported,
|
|
||||||
reflect.Func: decodeUnsupported,
|
|
||||||
reflect.Interface: decodeUnsupported,
|
|
||||||
reflect.Map: decodeUnsupported,
|
|
||||||
reflect.Ptr: decodeUnsupported,
|
|
||||||
reflect.Slice: decodeSlice,
|
|
||||||
reflect.String: decodeString,
|
|
||||||
reflect.Struct: decodeUnsupported,
|
|
||||||
reflect.UnsafePointer: decodeUnsupported,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global map of struct field specs that is populated once for every new
|
|
||||||
// struct type that is scanned. This caches the field types and the corresponding
|
|
||||||
// decoder functions to avoid iterating through struct fields on subsequent scans.
|
|
||||||
globalStructMap = newStructMap()
|
|
||||||
)
|
|
||||||
|
|
||||||
func Struct(dst interface{}) (StructValue, error) {
|
|
||||||
v := reflect.ValueOf(dst)
|
|
||||||
|
|
||||||
// The destination to scan into should be a struct pointer.
|
|
||||||
if v.Kind() != reflect.Ptr || v.IsNil() {
|
|
||||||
return StructValue{}, fmt.Errorf("redis.Scan(non-pointer %T)", dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
v = v.Elem()
|
|
||||||
if v.Kind() != reflect.Struct {
|
|
||||||
return StructValue{}, fmt.Errorf("redis.Scan(non-struct %T)", dst)
|
|
||||||
}
|
|
||||||
|
|
||||||
return StructValue{
|
|
||||||
spec: globalStructMap.get(v.Type()),
|
|
||||||
value: v,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan scans the results from a key-value Redis map result set to a destination struct.
|
|
||||||
// The Redis keys are matched to the struct's field with the `redis` tag.
|
|
||||||
func Scan(dst interface{}, keys []interface{}, vals []interface{}) error {
|
|
||||||
if len(keys) != len(vals) {
|
|
||||||
return errors.New("args should have the same number of keys and vals")
|
|
||||||
}
|
|
||||||
|
|
||||||
strct, err := Struct(dst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate through the (key, value) sequence.
|
|
||||||
for i := 0; i < len(vals); i++ {
|
|
||||||
key, ok := keys[i].(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
val, ok := vals[i].(string)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := strct.Scan(key, val); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeBool(f reflect.Value, s string) error {
|
|
||||||
b, err := strconv.ParseBool(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.SetBool(b)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeInt8(f reflect.Value, s string) error {
|
|
||||||
return decodeNumber(f, s, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeInt16(f reflect.Value, s string) error {
|
|
||||||
return decodeNumber(f, s, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeInt32(f reflect.Value, s string) error {
|
|
||||||
return decodeNumber(f, s, 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeInt64(f reflect.Value, s string) error {
|
|
||||||
return decodeNumber(f, s, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeInt(f reflect.Value, s string) error {
|
|
||||||
return decodeNumber(f, s, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeNumber(f reflect.Value, s string, bitSize int) error {
|
|
||||||
v, err := strconv.ParseInt(s, 10, bitSize)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.SetInt(v)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUint8(f reflect.Value, s string) error {
|
|
||||||
return decodeUnsignedNumber(f, s, 8)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUint16(f reflect.Value, s string) error {
|
|
||||||
return decodeUnsignedNumber(f, s, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUint32(f reflect.Value, s string) error {
|
|
||||||
return decodeUnsignedNumber(f, s, 32)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUint64(f reflect.Value, s string) error {
|
|
||||||
return decodeUnsignedNumber(f, s, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUint(f reflect.Value, s string) error {
|
|
||||||
return decodeUnsignedNumber(f, s, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUnsignedNumber(f reflect.Value, s string, bitSize int) error {
|
|
||||||
v, err := strconv.ParseUint(s, 10, bitSize)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.SetUint(v)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeFloat32(f reflect.Value, s string) error {
|
|
||||||
v, err := strconv.ParseFloat(s, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.SetFloat(v)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// although the default is float64, but we better define it.
|
|
||||||
func decodeFloat64(f reflect.Value, s string) error {
|
|
||||||
v, err := strconv.ParseFloat(s, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
f.SetFloat(v)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeString(f reflect.Value, s string) error {
|
|
||||||
f.SetString(s)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeSlice(f reflect.Value, s string) error {
|
|
||||||
// []byte slice ([]uint8).
|
|
||||||
if f.Type().Elem().Kind() == reflect.Uint8 {
|
|
||||||
f.SetBytes([]byte(s))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUnsupported(v reflect.Value, s string) error {
|
|
||||||
return fmt.Errorf("redis.Scan(unsupported %s)", v.Type())
|
|
||||||
}
|
|
@ -1,93 +0,0 @@
|
|||||||
package hscan
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// structMap contains the map of struct fields for target structs
|
|
||||||
// indexed by the struct type.
|
|
||||||
type structMap struct {
|
|
||||||
m sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStructMap() *structMap {
|
|
||||||
return new(structMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *structMap) get(t reflect.Type) *structSpec {
|
|
||||||
if v, ok := s.m.Load(t); ok {
|
|
||||||
return v.(*structSpec)
|
|
||||||
}
|
|
||||||
|
|
||||||
spec := newStructSpec(t, "redis")
|
|
||||||
s.m.Store(t, spec)
|
|
||||||
return spec
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// structSpec contains the list of all fields in a target struct.
|
|
||||||
type structSpec struct {
|
|
||||||
m map[string]*structField
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *structSpec) set(tag string, sf *structField) {
|
|
||||||
s.m[tag] = sf
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStructSpec(t reflect.Type, fieldTag string) *structSpec {
|
|
||||||
numField := t.NumField()
|
|
||||||
out := &structSpec{
|
|
||||||
m: make(map[string]*structField, numField),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < numField; i++ {
|
|
||||||
f := t.Field(i)
|
|
||||||
|
|
||||||
tag := f.Tag.Get(fieldTag)
|
|
||||||
if tag == "" || tag == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
tag = strings.Split(tag, ",")[0]
|
|
||||||
if tag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the built-in decoder.
|
|
||||||
out.set(tag, &structField{index: i, fn: decoders[f.Type.Kind()]})
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// structField represents a single field in a target struct.
|
|
||||||
type structField struct {
|
|
||||||
index int
|
|
||||||
fn decoderFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type StructValue struct {
|
|
||||||
spec *structSpec
|
|
||||||
value reflect.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s StructValue) Scan(key string, value string) error {
|
|
||||||
field, ok := s.spec.m[key]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := field.fn(s.value.Field(field.index), value); err != nil {
|
|
||||||
t := s.value.Type()
|
|
||||||
return fmt.Errorf("cannot scan redis.result %s into struct field %s.%s of type %s, error-%s",
|
|
||||||
value, t.Name(), t.Field(field.index).Name, t.Field(field.index).Type, err.Error())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
func RetryBackoff(retry int, minBackoff, maxBackoff time.Duration) time.Duration {
|
|
||||||
if retry < 0 {
|
|
||||||
panic("not reached")
|
|
||||||
}
|
|
||||||
if minBackoff == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
d := minBackoff << uint(retry)
|
|
||||||
if d < minBackoff {
|
|
||||||
return maxBackoff
|
|
||||||
}
|
|
||||||
|
|
||||||
d = minBackoff + time.Duration(rand.Int63n(int64(d)))
|
|
||||||
|
|
||||||
if d > maxBackoff || d < minBackoff {
|
|
||||||
d = maxBackoff
|
|
||||||
}
|
|
||||||
|
|
||||||
return d
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Logging interface {
|
|
||||||
Printf(ctx context.Context, format string, v ...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
type logger struct {
|
|
||||||
log *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *logger) Printf(ctx context.Context, format string, v ...interface{}) {
|
|
||||||
_ = l.log.Output(2, fmt.Sprintf(format, v...))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger calls Output to print to the stderr.
|
|
||||||
// Arguments are handled in the manner of fmt.Print.
|
|
||||||
var Logger Logging = &logger{
|
|
||||||
log: log.New(os.Stderr, "redis: ", log.LstdFlags|log.Lshortfile),
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2014 The Camlistore 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 internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Once will perform a successful action exactly once.
|
|
||||||
//
|
|
||||||
// Unlike a sync.Once, this Once's func returns an error
|
|
||||||
// and is re-armed on failure.
|
|
||||||
type Once struct {
|
|
||||||
m sync.Mutex
|
|
||||||
done uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do calls the function f if and only if Do has not been invoked
|
|
||||||
// without error for this instance of Once. In other words, given
|
|
||||||
// var once Once
|
|
||||||
// if once.Do(f) is called multiple times, only the first call will
|
|
||||||
// invoke f, even if f has a different value in each invocation unless
|
|
||||||
// f returns an error. A new instance of Once is required for each
|
|
||||||
// function to execute.
|
|
||||||
//
|
|
||||||
// Do is intended for initialization that must be run exactly once. Since f
|
|
||||||
// is niladic, it may be necessary to use a function literal to capture the
|
|
||||||
// arguments to a function to be invoked by Do:
|
|
||||||
// err := config.once.Do(func() error { return config.init(filename) })
|
|
||||||
func (o *Once) Do(f func() error) error {
|
|
||||||
if atomic.LoadUint32(&o.done) == 1 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Slow-path.
|
|
||||||
o.m.Lock()
|
|
||||||
defer o.m.Unlock()
|
|
||||||
var err error
|
|
||||||
if o.done == 0 {
|
|
||||||
err = f()
|
|
||||||
if err == nil {
|
|
||||||
atomic.StoreUint32(&o.done, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
package pool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
var noDeadline = time.Time{}
|
|
||||||
|
|
||||||
type Conn struct {
|
|
||||||
usedAt int64 // atomic
|
|
||||||
netConn net.Conn
|
|
||||||
|
|
||||||
rd *proto.Reader
|
|
||||||
bw *bufio.Writer
|
|
||||||
wr *proto.Writer
|
|
||||||
|
|
||||||
Inited bool
|
|
||||||
pooled bool
|
|
||||||
createdAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConn(netConn net.Conn) *Conn {
|
|
||||||
cn := &Conn{
|
|
||||||
netConn: netConn,
|
|
||||||
createdAt: time.Now(),
|
|
||||||
}
|
|
||||||
cn.rd = proto.NewReader(netConn)
|
|
||||||
cn.bw = bufio.NewWriter(netConn)
|
|
||||||
cn.wr = proto.NewWriter(cn.bw)
|
|
||||||
cn.SetUsedAt(time.Now())
|
|
||||||
return cn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) UsedAt() time.Time {
|
|
||||||
unix := atomic.LoadInt64(&cn.usedAt)
|
|
||||||
return time.Unix(unix, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) SetUsedAt(tm time.Time) {
|
|
||||||
atomic.StoreInt64(&cn.usedAt, tm.Unix())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) SetNetConn(netConn net.Conn) {
|
|
||||||
cn.netConn = netConn
|
|
||||||
cn.rd.Reset(netConn)
|
|
||||||
cn.bw.Reset(netConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) Write(b []byte) (int, error) {
|
|
||||||
return cn.netConn.Write(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) RemoteAddr() net.Addr {
|
|
||||||
if cn.netConn != nil {
|
|
||||||
return cn.netConn.RemoteAddr()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) WithReader(ctx context.Context, timeout time.Duration, fn func(rd *proto.Reader) error) error {
|
|
||||||
if err := cn.netConn.SetReadDeadline(cn.deadline(ctx, timeout)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return fn(cn.rd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) WithWriter(
|
|
||||||
ctx context.Context, timeout time.Duration, fn func(wr *proto.Writer) error,
|
|
||||||
) error {
|
|
||||||
if err := cn.netConn.SetWriteDeadline(cn.deadline(ctx, timeout)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cn.bw.Buffered() > 0 {
|
|
||||||
cn.bw.Reset(cn.netConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fn(cn.wr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cn.bw.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) Close() error {
|
|
||||||
return cn.netConn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cn *Conn) deadline(ctx context.Context, timeout time.Duration) time.Time {
|
|
||||||
tm := time.Now()
|
|
||||||
cn.SetUsedAt(tm)
|
|
||||||
|
|
||||||
if timeout > 0 {
|
|
||||||
tm = tm.Add(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx != nil {
|
|
||||||
deadline, ok := ctx.Deadline()
|
|
||||||
if ok {
|
|
||||||
if timeout == 0 {
|
|
||||||
return deadline
|
|
||||||
}
|
|
||||||
if deadline.Before(tm) {
|
|
||||||
return deadline
|
|
||||||
}
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if timeout > 0 {
|
|
||||||
return tm
|
|
||||||
}
|
|
||||||
|
|
||||||
return noDeadline
|
|
||||||
}
|
|
@ -1,557 +0,0 @@
|
|||||||
package pool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrClosed performs any operation on the closed client will return this error.
|
|
||||||
ErrClosed = errors.New("redis: client is closed")
|
|
||||||
|
|
||||||
// ErrPoolTimeout timed out waiting to get a connection from the connection pool.
|
|
||||||
ErrPoolTimeout = errors.New("redis: connection pool timeout")
|
|
||||||
)
|
|
||||||
|
|
||||||
var timers = sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
t := time.NewTimer(time.Hour)
|
|
||||||
t.Stop()
|
|
||||||
return t
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stats contains pool state information and accumulated stats.
|
|
||||||
type Stats struct {
|
|
||||||
Hits uint32 // number of times free connection was found in the pool
|
|
||||||
Misses uint32 // number of times free connection was NOT found in the pool
|
|
||||||
Timeouts uint32 // number of times a wait timeout occurred
|
|
||||||
|
|
||||||
TotalConns uint32 // number of total connections in the pool
|
|
||||||
IdleConns uint32 // number of idle connections in the pool
|
|
||||||
StaleConns uint32 // number of stale connections removed from the pool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Pooler interface {
|
|
||||||
NewConn(context.Context) (*Conn, error)
|
|
||||||
CloseConn(*Conn) error
|
|
||||||
|
|
||||||
Get(context.Context) (*Conn, error)
|
|
||||||
Put(context.Context, *Conn)
|
|
||||||
Remove(context.Context, *Conn, error)
|
|
||||||
|
|
||||||
Len() int
|
|
||||||
IdleLen() int
|
|
||||||
Stats() *Stats
|
|
||||||
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Dialer func(context.Context) (net.Conn, error)
|
|
||||||
OnClose func(*Conn) error
|
|
||||||
|
|
||||||
PoolFIFO bool
|
|
||||||
PoolSize int
|
|
||||||
MinIdleConns int
|
|
||||||
MaxConnAge time.Duration
|
|
||||||
PoolTimeout time.Duration
|
|
||||||
IdleTimeout time.Duration
|
|
||||||
IdleCheckFrequency time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
type lastDialErrorWrap struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type ConnPool struct {
|
|
||||||
opt *Options
|
|
||||||
|
|
||||||
dialErrorsNum uint32 // atomic
|
|
||||||
|
|
||||||
lastDialError atomic.Value
|
|
||||||
|
|
||||||
queue chan struct{}
|
|
||||||
|
|
||||||
connsMu sync.Mutex
|
|
||||||
conns []*Conn
|
|
||||||
idleConns []*Conn
|
|
||||||
poolSize int
|
|
||||||
idleConnsLen int
|
|
||||||
|
|
||||||
stats Stats
|
|
||||||
|
|
||||||
_closed uint32 // atomic
|
|
||||||
closedCh chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Pooler = (*ConnPool)(nil)
|
|
||||||
|
|
||||||
func NewConnPool(opt *Options) *ConnPool {
|
|
||||||
p := &ConnPool{
|
|
||||||
opt: opt,
|
|
||||||
|
|
||||||
queue: make(chan struct{}, opt.PoolSize),
|
|
||||||
conns: make([]*Conn, 0, opt.PoolSize),
|
|
||||||
idleConns: make([]*Conn, 0, opt.PoolSize),
|
|
||||||
closedCh: make(chan struct{}),
|
|
||||||
}
|
|
||||||
|
|
||||||
p.connsMu.Lock()
|
|
||||||
p.checkMinIdleConns()
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
|
|
||||||
if opt.IdleTimeout > 0 && opt.IdleCheckFrequency > 0 {
|
|
||||||
go p.reaper(opt.IdleCheckFrequency)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) checkMinIdleConns() {
|
|
||||||
if p.opt.MinIdleConns == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for p.poolSize < p.opt.PoolSize && p.idleConnsLen < p.opt.MinIdleConns {
|
|
||||||
p.poolSize++
|
|
||||||
p.idleConnsLen++
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
err := p.addIdleConn()
|
|
||||||
if err != nil && err != ErrClosed {
|
|
||||||
p.connsMu.Lock()
|
|
||||||
p.poolSize--
|
|
||||||
p.idleConnsLen--
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) addIdleConn() error {
|
|
||||||
cn, err := p.dialConn(context.TODO(), true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.connsMu.Lock()
|
|
||||||
defer p.connsMu.Unlock()
|
|
||||||
|
|
||||||
// It is not allowed to add new connections to the closed connection pool.
|
|
||||||
if p.closed() {
|
|
||||||
_ = cn.Close()
|
|
||||||
return ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
p.conns = append(p.conns, cn)
|
|
||||||
p.idleConns = append(p.idleConns, cn)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) NewConn(ctx context.Context) (*Conn, error) {
|
|
||||||
return p.newConn(ctx, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) newConn(ctx context.Context, pooled bool) (*Conn, error) {
|
|
||||||
cn, err := p.dialConn(ctx, pooled)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
p.connsMu.Lock()
|
|
||||||
defer p.connsMu.Unlock()
|
|
||||||
|
|
||||||
// It is not allowed to add new connections to the closed connection pool.
|
|
||||||
if p.closed() {
|
|
||||||
_ = cn.Close()
|
|
||||||
return nil, ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
p.conns = append(p.conns, cn)
|
|
||||||
if pooled {
|
|
||||||
// If pool is full remove the cn on next Put.
|
|
||||||
if p.poolSize >= p.opt.PoolSize {
|
|
||||||
cn.pooled = false
|
|
||||||
} else {
|
|
||||||
p.poolSize++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) dialConn(ctx context.Context, pooled bool) (*Conn, error) {
|
|
||||||
if p.closed() {
|
|
||||||
return nil, ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
if atomic.LoadUint32(&p.dialErrorsNum) >= uint32(p.opt.PoolSize) {
|
|
||||||
return nil, p.getLastDialError()
|
|
||||||
}
|
|
||||||
|
|
||||||
netConn, err := p.opt.Dialer(ctx)
|
|
||||||
if err != nil {
|
|
||||||
p.setLastDialError(err)
|
|
||||||
if atomic.AddUint32(&p.dialErrorsNum, 1) == uint32(p.opt.PoolSize) {
|
|
||||||
go p.tryDial()
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cn := NewConn(netConn)
|
|
||||||
cn.pooled = pooled
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) tryDial() {
|
|
||||||
for {
|
|
||||||
if p.closed() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := p.opt.Dialer(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
p.setLastDialError(err)
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.StoreUint32(&p.dialErrorsNum, 0)
|
|
||||||
_ = conn.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) setLastDialError(err error) {
|
|
||||||
p.lastDialError.Store(&lastDialErrorWrap{err: err})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) getLastDialError() error {
|
|
||||||
err, _ := p.lastDialError.Load().(*lastDialErrorWrap)
|
|
||||||
if err != nil {
|
|
||||||
return err.err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns existed connection from the pool or creates a new one.
|
|
||||||
func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
|
|
||||||
if p.closed() {
|
|
||||||
return nil, ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := p.waitTurn(ctx); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
p.connsMu.Lock()
|
|
||||||
cn, err := p.popIdle()
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cn == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.isStaleConn(cn) {
|
|
||||||
_ = p.CloseConn(cn)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddUint32(&p.stats.Hits, 1)
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddUint32(&p.stats.Misses, 1)
|
|
||||||
|
|
||||||
newcn, err := p.newConn(ctx, true)
|
|
||||||
if err != nil {
|
|
||||||
p.freeTurn()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newcn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) getTurn() {
|
|
||||||
p.queue <- struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) waitTurn(ctx context.Context) error {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case p.queue <- struct{}{}:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
timer := timers.Get().(*time.Timer)
|
|
||||||
timer.Reset(p.opt.PoolTimeout)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
timers.Put(timer)
|
|
||||||
return ctx.Err()
|
|
||||||
case p.queue <- struct{}{}:
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
timers.Put(timer)
|
|
||||||
return nil
|
|
||||||
case <-timer.C:
|
|
||||||
timers.Put(timer)
|
|
||||||
atomic.AddUint32(&p.stats.Timeouts, 1)
|
|
||||||
return ErrPoolTimeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) freeTurn() {
|
|
||||||
<-p.queue
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) popIdle() (*Conn, error) {
|
|
||||||
if p.closed() {
|
|
||||||
return nil, ErrClosed
|
|
||||||
}
|
|
||||||
n := len(p.idleConns)
|
|
||||||
if n == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var cn *Conn
|
|
||||||
if p.opt.PoolFIFO {
|
|
||||||
cn = p.idleConns[0]
|
|
||||||
copy(p.idleConns, p.idleConns[1:])
|
|
||||||
p.idleConns = p.idleConns[:n-1]
|
|
||||||
} else {
|
|
||||||
idx := n - 1
|
|
||||||
cn = p.idleConns[idx]
|
|
||||||
p.idleConns = p.idleConns[:idx]
|
|
||||||
}
|
|
||||||
p.idleConnsLen--
|
|
||||||
p.checkMinIdleConns()
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) Put(ctx context.Context, cn *Conn) {
|
|
||||||
if cn.rd.Buffered() > 0 {
|
|
||||||
internal.Logger.Printf(ctx, "Conn has unread data")
|
|
||||||
p.Remove(ctx, cn, BadConnError{})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cn.pooled {
|
|
||||||
p.Remove(ctx, cn, nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.connsMu.Lock()
|
|
||||||
p.idleConns = append(p.idleConns, cn)
|
|
||||||
p.idleConnsLen++
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
p.freeTurn()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) Remove(ctx context.Context, cn *Conn, reason error) {
|
|
||||||
p.removeConnWithLock(cn)
|
|
||||||
p.freeTurn()
|
|
||||||
_ = p.closeConn(cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) CloseConn(cn *Conn) error {
|
|
||||||
p.removeConnWithLock(cn)
|
|
||||||
return p.closeConn(cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) removeConnWithLock(cn *Conn) {
|
|
||||||
p.connsMu.Lock()
|
|
||||||
p.removeConn(cn)
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) removeConn(cn *Conn) {
|
|
||||||
for i, c := range p.conns {
|
|
||||||
if c == cn {
|
|
||||||
p.conns = append(p.conns[:i], p.conns[i+1:]...)
|
|
||||||
if cn.pooled {
|
|
||||||
p.poolSize--
|
|
||||||
p.checkMinIdleConns()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) closeConn(cn *Conn) error {
|
|
||||||
if p.opt.OnClose != nil {
|
|
||||||
_ = p.opt.OnClose(cn)
|
|
||||||
}
|
|
||||||
return cn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns total number of connections.
|
|
||||||
func (p *ConnPool) Len() int {
|
|
||||||
p.connsMu.Lock()
|
|
||||||
n := len(p.conns)
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
// IdleLen returns number of idle connections.
|
|
||||||
func (p *ConnPool) IdleLen() int {
|
|
||||||
p.connsMu.Lock()
|
|
||||||
n := p.idleConnsLen
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) Stats() *Stats {
|
|
||||||
idleLen := p.IdleLen()
|
|
||||||
return &Stats{
|
|
||||||
Hits: atomic.LoadUint32(&p.stats.Hits),
|
|
||||||
Misses: atomic.LoadUint32(&p.stats.Misses),
|
|
||||||
Timeouts: atomic.LoadUint32(&p.stats.Timeouts),
|
|
||||||
|
|
||||||
TotalConns: uint32(p.Len()),
|
|
||||||
IdleConns: uint32(idleLen),
|
|
||||||
StaleConns: atomic.LoadUint32(&p.stats.StaleConns),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) closed() bool {
|
|
||||||
return atomic.LoadUint32(&p._closed) == 1
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) Filter(fn func(*Conn) bool) error {
|
|
||||||
p.connsMu.Lock()
|
|
||||||
defer p.connsMu.Unlock()
|
|
||||||
|
|
||||||
var firstErr error
|
|
||||||
for _, cn := range p.conns {
|
|
||||||
if fn(cn) {
|
|
||||||
if err := p.closeConn(cn); err != nil && firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) Close() error {
|
|
||||||
if !atomic.CompareAndSwapUint32(&p._closed, 0, 1) {
|
|
||||||
return ErrClosed
|
|
||||||
}
|
|
||||||
close(p.closedCh)
|
|
||||||
|
|
||||||
var firstErr error
|
|
||||||
p.connsMu.Lock()
|
|
||||||
for _, cn := range p.conns {
|
|
||||||
if err := p.closeConn(cn); err != nil && firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
p.conns = nil
|
|
||||||
p.poolSize = 0
|
|
||||||
p.idleConns = nil
|
|
||||||
p.idleConnsLen = 0
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) reaper(frequency time.Duration) {
|
|
||||||
ticker := time.NewTicker(frequency)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
// It is possible that ticker and closedCh arrive together,
|
|
||||||
// and select pseudo-randomly pick ticker case, we double
|
|
||||||
// check here to prevent being executed after closed.
|
|
||||||
if p.closed() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, err := p.ReapStaleConns()
|
|
||||||
if err != nil {
|
|
||||||
internal.Logger.Printf(context.Background(), "ReapStaleConns failed: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
case <-p.closedCh:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) ReapStaleConns() (int, error) {
|
|
||||||
var n int
|
|
||||||
for {
|
|
||||||
p.getTurn()
|
|
||||||
|
|
||||||
p.connsMu.Lock()
|
|
||||||
cn := p.reapStaleConn()
|
|
||||||
p.connsMu.Unlock()
|
|
||||||
|
|
||||||
p.freeTurn()
|
|
||||||
|
|
||||||
if cn != nil {
|
|
||||||
_ = p.closeConn(cn)
|
|
||||||
n++
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
atomic.AddUint32(&p.stats.StaleConns, uint32(n))
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) reapStaleConn() *Conn {
|
|
||||||
if len(p.idleConns) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cn := p.idleConns[0]
|
|
||||||
if !p.isStaleConn(cn) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
p.idleConns = append(p.idleConns[:0], p.idleConns[1:]...)
|
|
||||||
p.idleConnsLen--
|
|
||||||
p.removeConn(cn)
|
|
||||||
|
|
||||||
return cn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ConnPool) isStaleConn(cn *Conn) bool {
|
|
||||||
if p.opt.IdleTimeout == 0 && p.opt.MaxConnAge == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
if p.opt.IdleTimeout > 0 && now.Sub(cn.UsedAt()) >= p.opt.IdleTimeout {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if p.opt.MaxConnAge > 0 && now.Sub(cn.createdAt) >= p.opt.MaxConnAge {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package pool
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type SingleConnPool struct {
|
|
||||||
pool Pooler
|
|
||||||
cn *Conn
|
|
||||||
stickyErr error
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Pooler = (*SingleConnPool)(nil)
|
|
||||||
|
|
||||||
func NewSingleConnPool(pool Pooler, cn *Conn) *SingleConnPool {
|
|
||||||
return &SingleConnPool{
|
|
||||||
pool: pool,
|
|
||||||
cn: cn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) NewConn(ctx context.Context) (*Conn, error) {
|
|
||||||
return p.pool.NewConn(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) CloseConn(cn *Conn) error {
|
|
||||||
return p.pool.CloseConn(cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) Get(ctx context.Context) (*Conn, error) {
|
|
||||||
if p.stickyErr != nil {
|
|
||||||
return nil, p.stickyErr
|
|
||||||
}
|
|
||||||
return p.cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) Put(ctx context.Context, cn *Conn) {}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) Remove(ctx context.Context, cn *Conn, reason error) {
|
|
||||||
p.cn = nil
|
|
||||||
p.stickyErr = reason
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) Close() error {
|
|
||||||
p.cn = nil
|
|
||||||
p.stickyErr = ErrClosed
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) Len() int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) IdleLen() int {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SingleConnPool) Stats() *Stats {
|
|
||||||
return &Stats{}
|
|
||||||
}
|
|
@ -1,201 +0,0 @@
|
|||||||
package pool
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync/atomic"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
stateDefault = 0
|
|
||||||
stateInited = 1
|
|
||||||
stateClosed = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
type BadConnError struct {
|
|
||||||
wrapped error
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ error = (*BadConnError)(nil)
|
|
||||||
|
|
||||||
func (e BadConnError) Error() string {
|
|
||||||
s := "redis: Conn is in a bad state"
|
|
||||||
if e.wrapped != nil {
|
|
||||||
s += ": " + e.wrapped.Error()
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e BadConnError) Unwrap() error {
|
|
||||||
return e.wrapped
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type StickyConnPool struct {
|
|
||||||
pool Pooler
|
|
||||||
shared int32 // atomic
|
|
||||||
|
|
||||||
state uint32 // atomic
|
|
||||||
ch chan *Conn
|
|
||||||
|
|
||||||
_badConnError atomic.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Pooler = (*StickyConnPool)(nil)
|
|
||||||
|
|
||||||
func NewStickyConnPool(pool Pooler) *StickyConnPool {
|
|
||||||
p, ok := pool.(*StickyConnPool)
|
|
||||||
if !ok {
|
|
||||||
p = &StickyConnPool{
|
|
||||||
pool: pool,
|
|
||||||
ch: make(chan *Conn, 1),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
atomic.AddInt32(&p.shared, 1)
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) NewConn(ctx context.Context) (*Conn, error) {
|
|
||||||
return p.pool.NewConn(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) CloseConn(cn *Conn) error {
|
|
||||||
return p.pool.CloseConn(cn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) Get(ctx context.Context) (*Conn, error) {
|
|
||||||
// In worst case this races with Close which is not a very common operation.
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
switch atomic.LoadUint32(&p.state) {
|
|
||||||
case stateDefault:
|
|
||||||
cn, err := p.pool.Get(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if atomic.CompareAndSwapUint32(&p.state, stateDefault, stateInited) {
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
p.pool.Remove(ctx, cn, ErrClosed)
|
|
||||||
case stateInited:
|
|
||||||
if err := p.badConnError(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cn, ok := <-p.ch
|
|
||||||
if !ok {
|
|
||||||
return nil, ErrClosed
|
|
||||||
}
|
|
||||||
return cn, nil
|
|
||||||
case stateClosed:
|
|
||||||
return nil, ErrClosed
|
|
||||||
default:
|
|
||||||
panic("not reached")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("redis: StickyConnPool.Get: infinite loop")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) Put(ctx context.Context, cn *Conn) {
|
|
||||||
defer func() {
|
|
||||||
if recover() != nil {
|
|
||||||
p.freeConn(ctx, cn)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
p.ch <- cn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) freeConn(ctx context.Context, cn *Conn) {
|
|
||||||
if err := p.badConnError(); err != nil {
|
|
||||||
p.pool.Remove(ctx, cn, err)
|
|
||||||
} else {
|
|
||||||
p.pool.Put(ctx, cn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) Remove(ctx context.Context, cn *Conn, reason error) {
|
|
||||||
defer func() {
|
|
||||||
if recover() != nil {
|
|
||||||
p.pool.Remove(ctx, cn, ErrClosed)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
p._badConnError.Store(BadConnError{wrapped: reason})
|
|
||||||
p.ch <- cn
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) Close() error {
|
|
||||||
if shared := atomic.AddInt32(&p.shared, -1); shared > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < 1000; i++ {
|
|
||||||
state := atomic.LoadUint32(&p.state)
|
|
||||||
if state == stateClosed {
|
|
||||||
return ErrClosed
|
|
||||||
}
|
|
||||||
if atomic.CompareAndSwapUint32(&p.state, state, stateClosed) {
|
|
||||||
close(p.ch)
|
|
||||||
cn, ok := <-p.ch
|
|
||||||
if ok {
|
|
||||||
p.freeConn(context.TODO(), cn)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("redis: StickyConnPool.Close: infinite loop")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) Reset(ctx context.Context) error {
|
|
||||||
if p.badConnError() == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case cn, ok := <-p.ch:
|
|
||||||
if !ok {
|
|
||||||
return ErrClosed
|
|
||||||
}
|
|
||||||
p.pool.Remove(ctx, cn, ErrClosed)
|
|
||||||
p._badConnError.Store(BadConnError{wrapped: nil})
|
|
||||||
default:
|
|
||||||
return errors.New("redis: StickyConnPool does not have a Conn")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !atomic.CompareAndSwapUint32(&p.state, stateInited, stateDefault) {
|
|
||||||
state := atomic.LoadUint32(&p.state)
|
|
||||||
return fmt.Errorf("redis: invalid StickyConnPool state: %d", state)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) badConnError() error {
|
|
||||||
if v := p._badConnError.Load(); v != nil {
|
|
||||||
if err := v.(BadConnError); err.wrapped != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) Len() int {
|
|
||||||
switch atomic.LoadUint32(&p.state) {
|
|
||||||
case stateDefault:
|
|
||||||
return 0
|
|
||||||
case stateInited:
|
|
||||||
return 1
|
|
||||||
case stateClosed:
|
|
||||||
return 0
|
|
||||||
default:
|
|
||||||
panic("not reached")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) IdleLen() int {
|
|
||||||
return len(p.ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *StickyConnPool) Stats() *Stats {
|
|
||||||
return &Stats{}
|
|
||||||
}
|
|
@ -1,332 +0,0 @@
|
|||||||
package proto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// redis resp protocol data type.
|
|
||||||
const (
|
|
||||||
ErrorReply = '-'
|
|
||||||
StatusReply = '+'
|
|
||||||
IntReply = ':'
|
|
||||||
StringReply = '$'
|
|
||||||
ArrayReply = '*'
|
|
||||||
)
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const Nil = RedisError("redis: nil") // nolint:errname
|
|
||||||
|
|
||||||
type RedisError string
|
|
||||||
|
|
||||||
func (e RedisError) Error() string { return string(e) }
|
|
||||||
|
|
||||||
func (RedisError) RedisError() {}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type MultiBulkParse func(*Reader, int64) (interface{}, error)
|
|
||||||
|
|
||||||
type Reader struct {
|
|
||||||
rd *bufio.Reader
|
|
||||||
_buf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReader(rd io.Reader) *Reader {
|
|
||||||
return &Reader{
|
|
||||||
rd: bufio.NewReader(rd),
|
|
||||||
_buf: make([]byte, 64),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) Buffered() int {
|
|
||||||
return r.rd.Buffered()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) Peek(n int) ([]byte, error) {
|
|
||||||
return r.rd.Peek(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) Reset(rd io.Reader) {
|
|
||||||
r.rd.Reset(rd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadLine() ([]byte, error) {
|
|
||||||
line, err := r.readLine()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isNilReply(line) {
|
|
||||||
return nil, Nil
|
|
||||||
}
|
|
||||||
return line, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readLine that returns an error if:
|
|
||||||
// - there is a pending read error;
|
|
||||||
// - or line does not end with \r\n.
|
|
||||||
func (r *Reader) readLine() ([]byte, error) {
|
|
||||||
b, err := r.rd.ReadSlice('\n')
|
|
||||||
if err != nil {
|
|
||||||
if err != bufio.ErrBufferFull {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
full := make([]byte, len(b))
|
|
||||||
copy(full, b)
|
|
||||||
|
|
||||||
b, err = r.rd.ReadBytes('\n')
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
full = append(full, b...) //nolint:makezero
|
|
||||||
b = full
|
|
||||||
}
|
|
||||||
if len(b) <= 2 || b[len(b)-1] != '\n' || b[len(b)-2] != '\r' {
|
|
||||||
return nil, fmt.Errorf("redis: invalid reply: %q", b)
|
|
||||||
}
|
|
||||||
return b[:len(b)-2], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadReply(m MultiBulkParse) (interface{}, error) {
|
|
||||||
line, err := r.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch line[0] {
|
|
||||||
case ErrorReply:
|
|
||||||
return nil, ParseErrorReply(line)
|
|
||||||
case StatusReply:
|
|
||||||
return string(line[1:]), nil
|
|
||||||
case IntReply:
|
|
||||||
return util.ParseInt(line[1:], 10, 64)
|
|
||||||
case StringReply:
|
|
||||||
return r.readStringReply(line)
|
|
||||||
case ArrayReply:
|
|
||||||
n, err := parseArrayLen(line)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if m == nil {
|
|
||||||
err := fmt.Errorf("redis: got %.100q, but multi bulk parser is nil", line)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return m(r, n)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("redis: can't parse %.100q", line)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadIntReply() (int64, error) {
|
|
||||||
line, err := r.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
switch line[0] {
|
|
||||||
case ErrorReply:
|
|
||||||
return 0, ParseErrorReply(line)
|
|
||||||
case IntReply:
|
|
||||||
return util.ParseInt(line[1:], 10, 64)
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("redis: can't parse int reply: %.100q", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadString() (string, error) {
|
|
||||||
line, err := r.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
switch line[0] {
|
|
||||||
case ErrorReply:
|
|
||||||
return "", ParseErrorReply(line)
|
|
||||||
case StringReply:
|
|
||||||
return r.readStringReply(line)
|
|
||||||
case StatusReply:
|
|
||||||
return string(line[1:]), nil
|
|
||||||
case IntReply:
|
|
||||||
return string(line[1:]), nil
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("redis: can't parse reply=%.100q reading string", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readStringReply(line []byte) (string, error) {
|
|
||||||
if isNilReply(line) {
|
|
||||||
return "", Nil
|
|
||||||
}
|
|
||||||
|
|
||||||
replyLen, err := util.Atoi(line[1:])
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
b := make([]byte, replyLen+2)
|
|
||||||
_, err = io.ReadFull(r.rd, b)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.BytesToString(b[:replyLen]), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadArrayReply(m MultiBulkParse) (interface{}, error) {
|
|
||||||
line, err := r.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch line[0] {
|
|
||||||
case ErrorReply:
|
|
||||||
return nil, ParseErrorReply(line)
|
|
||||||
case ArrayReply:
|
|
||||||
n, err := parseArrayLen(line)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return m(r, n)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("redis: can't parse array reply: %.100q", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadArrayLen() (int, error) {
|
|
||||||
line, err := r.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
switch line[0] {
|
|
||||||
case ErrorReply:
|
|
||||||
return 0, ParseErrorReply(line)
|
|
||||||
case ArrayReply:
|
|
||||||
n, err := parseArrayLen(line)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return int(n), nil
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("redis: can't parse array reply: %.100q", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadScanReply() ([]string, uint64, error) {
|
|
||||||
n, err := r.ReadArrayLen()
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if n != 2 {
|
|
||||||
return nil, 0, fmt.Errorf("redis: got %d elements in scan reply, expected 2", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor, err := r.ReadUint()
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err = r.ReadArrayLen()
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
keys := make([]string, n)
|
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
key, err := r.ReadString()
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
keys[i] = key
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys, cursor, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadInt() (int64, error) {
|
|
||||||
b, err := r.readTmpBytesReply()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return util.ParseInt(b, 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadUint() (uint64, error) {
|
|
||||||
b, err := r.readTmpBytesReply()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return util.ParseUint(b, 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) ReadFloatReply() (float64, error) {
|
|
||||||
b, err := r.readTmpBytesReply()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return util.ParseFloat(b, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readTmpBytesReply() ([]byte, error) {
|
|
||||||
line, err := r.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
switch line[0] {
|
|
||||||
case ErrorReply:
|
|
||||||
return nil, ParseErrorReply(line)
|
|
||||||
case StringReply:
|
|
||||||
return r._readTmpBytesReply(line)
|
|
||||||
case StatusReply:
|
|
||||||
return line[1:], nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("redis: can't parse string reply: %.100q", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) _readTmpBytesReply(line []byte) ([]byte, error) {
|
|
||||||
if isNilReply(line) {
|
|
||||||
return nil, Nil
|
|
||||||
}
|
|
||||||
|
|
||||||
replyLen, err := util.Atoi(line[1:])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := r.buf(replyLen + 2)
|
|
||||||
_, err = io.ReadFull(r.rd, buf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf[:replyLen], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) buf(n int) []byte {
|
|
||||||
if n <= cap(r._buf) {
|
|
||||||
return r._buf[:n]
|
|
||||||
}
|
|
||||||
d := n - cap(r._buf)
|
|
||||||
r._buf = append(r._buf, make([]byte, d)...)
|
|
||||||
return r._buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func isNilReply(b []byte) bool {
|
|
||||||
return len(b) == 3 &&
|
|
||||||
(b[0] == StringReply || b[0] == ArrayReply) &&
|
|
||||||
b[1] == '-' && b[2] == '1'
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseErrorReply(line []byte) error {
|
|
||||||
return RedisError(string(line[1:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseArrayLen(line []byte) (int64, error) {
|
|
||||||
if isNilReply(line) {
|
|
||||||
return 0, Nil
|
|
||||||
}
|
|
||||||
return util.ParseInt(line[1:], 10, 64)
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
package proto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Scan parses bytes `b` to `v` with appropriate type.
|
|
||||||
//nolint:gocyclo
|
|
||||||
func Scan(b []byte, v interface{}) error {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case nil:
|
|
||||||
return fmt.Errorf("redis: Scan(nil)")
|
|
||||||
case *string:
|
|
||||||
*v = util.BytesToString(b)
|
|
||||||
return nil
|
|
||||||
case *[]byte:
|
|
||||||
*v = b
|
|
||||||
return nil
|
|
||||||
case *int:
|
|
||||||
var err error
|
|
||||||
*v, err = util.Atoi(b)
|
|
||||||
return err
|
|
||||||
case *int8:
|
|
||||||
n, err := util.ParseInt(b, 10, 8)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = int8(n)
|
|
||||||
return nil
|
|
||||||
case *int16:
|
|
||||||
n, err := util.ParseInt(b, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = int16(n)
|
|
||||||
return nil
|
|
||||||
case *int32:
|
|
||||||
n, err := util.ParseInt(b, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = int32(n)
|
|
||||||
return nil
|
|
||||||
case *int64:
|
|
||||||
n, err := util.ParseInt(b, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = n
|
|
||||||
return nil
|
|
||||||
case *uint:
|
|
||||||
n, err := util.ParseUint(b, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = uint(n)
|
|
||||||
return nil
|
|
||||||
case *uint8:
|
|
||||||
n, err := util.ParseUint(b, 10, 8)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = uint8(n)
|
|
||||||
return nil
|
|
||||||
case *uint16:
|
|
||||||
n, err := util.ParseUint(b, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = uint16(n)
|
|
||||||
return nil
|
|
||||||
case *uint32:
|
|
||||||
n, err := util.ParseUint(b, 10, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = uint32(n)
|
|
||||||
return nil
|
|
||||||
case *uint64:
|
|
||||||
n, err := util.ParseUint(b, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = n
|
|
||||||
return nil
|
|
||||||
case *float32:
|
|
||||||
n, err := util.ParseFloat(b, 32)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = float32(n)
|
|
||||||
return err
|
|
||||||
case *float64:
|
|
||||||
var err error
|
|
||||||
*v, err = util.ParseFloat(b, 64)
|
|
||||||
return err
|
|
||||||
case *bool:
|
|
||||||
*v = len(b) == 1 && b[0] == '1'
|
|
||||||
return nil
|
|
||||||
case *time.Time:
|
|
||||||
var err error
|
|
||||||
*v, err = time.Parse(time.RFC3339Nano, util.BytesToString(b))
|
|
||||||
return err
|
|
||||||
case *time.Duration:
|
|
||||||
n, err := util.ParseInt(b, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*v = time.Duration(n)
|
|
||||||
return nil
|
|
||||||
case encoding.BinaryUnmarshaler:
|
|
||||||
return v.UnmarshalBinary(b)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf(
|
|
||||||
"redis: can't unmarshal %T (consider implementing BinaryUnmarshaler)", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ScanSlice(data []string, slice interface{}) error {
|
|
||||||
v := reflect.ValueOf(slice)
|
|
||||||
if !v.IsValid() {
|
|
||||||
return fmt.Errorf("redis: ScanSlice(nil)")
|
|
||||||
}
|
|
||||||
if v.Kind() != reflect.Ptr {
|
|
||||||
return fmt.Errorf("redis: ScanSlice(non-pointer %T)", slice)
|
|
||||||
}
|
|
||||||
v = v.Elem()
|
|
||||||
if v.Kind() != reflect.Slice {
|
|
||||||
return fmt.Errorf("redis: ScanSlice(non-slice %T)", slice)
|
|
||||||
}
|
|
||||||
|
|
||||||
next := makeSliceNextElemFunc(v)
|
|
||||||
for i, s := range data {
|
|
||||||
elem := next()
|
|
||||||
if err := Scan([]byte(s), elem.Addr().Interface()); err != nil {
|
|
||||||
err = fmt.Errorf("redis: ScanSlice index=%d value=%q failed: %w", i, s, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeSliceNextElemFunc(v reflect.Value) func() reflect.Value {
|
|
||||||
elemType := v.Type().Elem()
|
|
||||||
|
|
||||||
if elemType.Kind() == reflect.Ptr {
|
|
||||||
elemType = elemType.Elem()
|
|
||||||
return func() reflect.Value {
|
|
||||||
if v.Len() < v.Cap() {
|
|
||||||
v.Set(v.Slice(0, v.Len()+1))
|
|
||||||
elem := v.Index(v.Len() - 1)
|
|
||||||
if elem.IsNil() {
|
|
||||||
elem.Set(reflect.New(elemType))
|
|
||||||
}
|
|
||||||
return elem.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
elem := reflect.New(elemType)
|
|
||||||
v.Set(reflect.Append(v, elem))
|
|
||||||
return elem.Elem()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
zero := reflect.Zero(elemType)
|
|
||||||
return func() reflect.Value {
|
|
||||||
if v.Len() < v.Cap() {
|
|
||||||
v.Set(v.Slice(0, v.Len()+1))
|
|
||||||
return v.Index(v.Len() - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
v.Set(reflect.Append(v, zero))
|
|
||||||
return v.Index(v.Len() - 1)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,155 +0,0 @@
|
|||||||
package proto
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type writer interface {
|
|
||||||
io.Writer
|
|
||||||
io.ByteWriter
|
|
||||||
// io.StringWriter
|
|
||||||
WriteString(s string) (n int, err error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Writer struct {
|
|
||||||
writer
|
|
||||||
|
|
||||||
lenBuf []byte
|
|
||||||
numBuf []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWriter(wr writer) *Writer {
|
|
||||||
return &Writer{
|
|
||||||
writer: wr,
|
|
||||||
|
|
||||||
lenBuf: make([]byte, 64),
|
|
||||||
numBuf: make([]byte, 64),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) WriteArgs(args []interface{}) error {
|
|
||||||
if err := w.WriteByte(ArrayReply); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.writeLen(len(args)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arg := range args {
|
|
||||||
if err := w.WriteArg(arg); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) writeLen(n int) error {
|
|
||||||
w.lenBuf = strconv.AppendUint(w.lenBuf[:0], uint64(n), 10)
|
|
||||||
w.lenBuf = append(w.lenBuf, '\r', '\n')
|
|
||||||
_, err := w.Write(w.lenBuf)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) WriteArg(v interface{}) error {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case nil:
|
|
||||||
return w.string("")
|
|
||||||
case string:
|
|
||||||
return w.string(v)
|
|
||||||
case []byte:
|
|
||||||
return w.bytes(v)
|
|
||||||
case int:
|
|
||||||
return w.int(int64(v))
|
|
||||||
case int8:
|
|
||||||
return w.int(int64(v))
|
|
||||||
case int16:
|
|
||||||
return w.int(int64(v))
|
|
||||||
case int32:
|
|
||||||
return w.int(int64(v))
|
|
||||||
case int64:
|
|
||||||
return w.int(v)
|
|
||||||
case uint:
|
|
||||||
return w.uint(uint64(v))
|
|
||||||
case uint8:
|
|
||||||
return w.uint(uint64(v))
|
|
||||||
case uint16:
|
|
||||||
return w.uint(uint64(v))
|
|
||||||
case uint32:
|
|
||||||
return w.uint(uint64(v))
|
|
||||||
case uint64:
|
|
||||||
return w.uint(v)
|
|
||||||
case float32:
|
|
||||||
return w.float(float64(v))
|
|
||||||
case float64:
|
|
||||||
return w.float(v)
|
|
||||||
case bool:
|
|
||||||
if v {
|
|
||||||
return w.int(1)
|
|
||||||
}
|
|
||||||
return w.int(0)
|
|
||||||
case time.Time:
|
|
||||||
w.numBuf = v.AppendFormat(w.numBuf[:0], time.RFC3339Nano)
|
|
||||||
return w.bytes(w.numBuf)
|
|
||||||
case time.Duration:
|
|
||||||
return w.int(v.Nanoseconds())
|
|
||||||
case encoding.BinaryMarshaler:
|
|
||||||
b, err := v.MarshalBinary()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return w.bytes(b)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf(
|
|
||||||
"redis: can't marshal %T (implement encoding.BinaryMarshaler)", v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) bytes(b []byte) error {
|
|
||||||
if err := w.WriteByte(StringReply); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.writeLen(len(b)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := w.Write(b); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.crlf()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) string(s string) error {
|
|
||||||
return w.bytes(util.StringToBytes(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) uint(n uint64) error {
|
|
||||||
w.numBuf = strconv.AppendUint(w.numBuf[:0], n, 10)
|
|
||||||
return w.bytes(w.numBuf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) int(n int64) error {
|
|
||||||
w.numBuf = strconv.AppendInt(w.numBuf[:0], n, 10)
|
|
||||||
return w.bytes(w.numBuf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) float(f float64) error {
|
|
||||||
w.numBuf = strconv.AppendFloat(w.numBuf[:0], f, 'f', -1, 64)
|
|
||||||
return w.bytes(w.numBuf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) crlf() error {
|
|
||||||
if err := w.WriteByte('\r'); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return w.WriteByte('\n')
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
package rand
|
|
||||||
|
|
||||||
import (
|
|
||||||
"math/rand"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Int returns a non-negative pseudo-random int.
|
|
||||||
func Int() int { return pseudo.Int() }
|
|
||||||
|
|
||||||
// Intn returns, as an int, a non-negative pseudo-random number in [0,n).
|
|
||||||
// It panics if n <= 0.
|
|
||||||
func Intn(n int) int { return pseudo.Intn(n) }
|
|
||||||
|
|
||||||
// Int63n returns, as an int64, a non-negative pseudo-random number in [0,n).
|
|
||||||
// It panics if n <= 0.
|
|
||||||
func Int63n(n int64) int64 { return pseudo.Int63n(n) }
|
|
||||||
|
|
||||||
// Perm returns, as a slice of n ints, a pseudo-random permutation of the integers [0,n).
|
|
||||||
func Perm(n int) []int { return pseudo.Perm(n) }
|
|
||||||
|
|
||||||
// Seed uses the provided seed value to initialize the default Source to a
|
|
||||||
// deterministic state. If Seed is not called, the generator behaves as if
|
|
||||||
// seeded by Seed(1).
|
|
||||||
func Seed(n int64) { pseudo.Seed(n) }
|
|
||||||
|
|
||||||
var pseudo = rand.New(&source{src: rand.NewSource(1)})
|
|
||||||
|
|
||||||
type source struct {
|
|
||||||
src rand.Source
|
|
||||||
mu sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *source) Int63() int64 {
|
|
||||||
s.mu.Lock()
|
|
||||||
n := s.src.Int63()
|
|
||||||
s.mu.Unlock()
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *source) Seed(seed int64) {
|
|
||||||
s.mu.Lock()
|
|
||||||
s.src.Seed(seed)
|
|
||||||
s.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shuffle pseudo-randomizes the order of elements.
|
|
||||||
// n is the number of elements.
|
|
||||||
// swap swaps the elements with indexes i and j.
|
|
||||||
func Shuffle(n int, swap func(i, j int)) { pseudo.Shuffle(n, swap) }
|
|
@ -1,12 +0,0 @@
|
|||||||
//go:build appengine
|
|
||||||
// +build appengine
|
|
||||||
|
|
||||||
package internal
|
|
||||||
|
|
||||||
func String(b []byte) string {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Bytes(s string) []byte {
|
|
||||||
return []byte(s)
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
//go:build !appengine
|
|
||||||
// +build !appengine
|
|
||||||
|
|
||||||
package internal
|
|
||||||
|
|
||||||
import "unsafe"
|
|
||||||
|
|
||||||
// String converts byte slice to string.
|
|
||||||
func String(b []byte) string {
|
|
||||||
return *(*string)(unsafe.Pointer(&b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bytes converts string to byte slice.
|
|
||||||
func Bytes(s string) []byte {
|
|
||||||
return *(*[]byte)(unsafe.Pointer(
|
|
||||||
&struct {
|
|
||||||
string
|
|
||||||
Cap int
|
|
||||||
}{s, len(s)},
|
|
||||||
))
|
|
||||||
}
|
|
@ -1,46 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Sleep(ctx context.Context, dur time.Duration) error {
|
|
||||||
t := time.NewTimer(dur)
|
|
||||||
defer t.Stop()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-t.C:
|
|
||||||
return nil
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ToLower(s string) string {
|
|
||||||
if isLower(s) {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
b := make([]byte, len(s))
|
|
||||||
for i := range b {
|
|
||||||
c := s[i]
|
|
||||||
if c >= 'A' && c <= 'Z' {
|
|
||||||
c += 'a' - 'A'
|
|
||||||
}
|
|
||||||
b[i] = c
|
|
||||||
}
|
|
||||||
return util.BytesToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLower(s string) bool {
|
|
||||||
for i := 0; i < len(s); i++ {
|
|
||||||
c := s[i]
|
|
||||||
if c >= 'A' && c <= 'Z' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
@ -1,12 +0,0 @@
|
|||||||
//go:build appengine
|
|
||||||
// +build appengine
|
|
||||||
|
|
||||||
package util
|
|
||||||
|
|
||||||
func BytesToString(b []byte) string {
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringToBytes(s string) []byte {
|
|
||||||
return []byte(s)
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
package util
|
|
||||||
|
|
||||||
import "strconv"
|
|
||||||
|
|
||||||
func Atoi(b []byte) (int, error) {
|
|
||||||
return strconv.Atoi(BytesToString(b))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseInt(b []byte, base int, bitSize int) (int64, error) {
|
|
||||||
return strconv.ParseInt(BytesToString(b), base, bitSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseUint(b []byte, base int, bitSize int) (uint64, error) {
|
|
||||||
return strconv.ParseUint(BytesToString(b), base, bitSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseFloat(b []byte, bitSize int) (float64, error) {
|
|
||||||
return strconv.ParseFloat(BytesToString(b), bitSize)
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
//go:build !appengine
|
|
||||||
// +build !appengine
|
|
||||||
|
|
||||||
package util
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BytesToString converts byte slice to string.
|
|
||||||
func BytesToString(b []byte) string {
|
|
||||||
return *(*string)(unsafe.Pointer(&b))
|
|
||||||
}
|
|
||||||
|
|
||||||
// StringToBytes converts string to byte slice.
|
|
||||||
func StringToBytes(s string) []byte {
|
|
||||||
return *(*[]byte)(unsafe.Pointer(
|
|
||||||
&struct {
|
|
||||||
string
|
|
||||||
Cap int
|
|
||||||
}{s, len(s)},
|
|
||||||
))
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ScanIterator is used to incrementally iterate over a collection of elements.
|
|
||||||
// It's safe for concurrent use by multiple goroutines.
|
|
||||||
type ScanIterator struct {
|
|
||||||
mu sync.Mutex // protects Scanner and pos
|
|
||||||
cmd *ScanCmd
|
|
||||||
pos int
|
|
||||||
}
|
|
||||||
|
|
||||||
// Err returns the last iterator error, if any.
|
|
||||||
func (it *ScanIterator) Err() error {
|
|
||||||
it.mu.Lock()
|
|
||||||
err := it.cmd.Err()
|
|
||||||
it.mu.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next advances the cursor and returns true if more values can be read.
|
|
||||||
func (it *ScanIterator) Next(ctx context.Context) bool {
|
|
||||||
it.mu.Lock()
|
|
||||||
defer it.mu.Unlock()
|
|
||||||
|
|
||||||
// Instantly return on errors.
|
|
||||||
if it.cmd.Err() != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Advance cursor, check if we are still within range.
|
|
||||||
if it.pos < len(it.cmd.page) {
|
|
||||||
it.pos++
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Return if there is no more data to fetch.
|
|
||||||
if it.cmd.cursor == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch next page.
|
|
||||||
switch it.cmd.args[0] {
|
|
||||||
case "scan", "qscan":
|
|
||||||
it.cmd.args[1] = it.cmd.cursor
|
|
||||||
default:
|
|
||||||
it.cmd.args[2] = it.cmd.cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
err := it.cmd.process(ctx, it.cmd)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
it.pos = 1
|
|
||||||
|
|
||||||
// Redis can occasionally return empty page.
|
|
||||||
if len(it.cmd.page) > 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Val returns the key/field at the current cursor position.
|
|
||||||
func (it *ScanIterator) Val() string {
|
|
||||||
var v string
|
|
||||||
it.mu.Lock()
|
|
||||||
if it.cmd.Err() == nil && it.pos > 0 && it.pos <= len(it.cmd.page) {
|
|
||||||
v = it.cmd.page[it.pos-1]
|
|
||||||
}
|
|
||||||
it.mu.Unlock()
|
|
||||||
return v
|
|
||||||
}
|
|
@ -1,429 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Limiter is the interface of a rate limiter or a circuit breaker.
|
|
||||||
type Limiter interface {
|
|
||||||
// Allow returns nil if operation is allowed or an error otherwise.
|
|
||||||
// If operation is allowed client must ReportResult of the operation
|
|
||||||
// whether it is a success or a failure.
|
|
||||||
Allow() error
|
|
||||||
// ReportResult reports the result of the previously allowed operation.
|
|
||||||
// nil indicates a success, non-nil error usually indicates a failure.
|
|
||||||
ReportResult(result error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options keeps the settings to setup redis connection.
|
|
||||||
type Options struct {
|
|
||||||
// The network type, either tcp or unix.
|
|
||||||
// Default is tcp.
|
|
||||||
Network string
|
|
||||||
// host:port address.
|
|
||||||
Addr string
|
|
||||||
|
|
||||||
// Dialer creates new network connection and has priority over
|
|
||||||
// Network and Addr options.
|
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
|
||||||
|
|
||||||
// Hook that is called when new connection is established.
|
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
|
||||||
|
|
||||||
// Use the specified Username to authenticate the current connection
|
|
||||||
// with one of the connections defined in the ACL list when connecting
|
|
||||||
// to a Redis 6.0 instance, or greater, that is using the Redis ACL system.
|
|
||||||
Username string
|
|
||||||
// Optional password. Must match the password specified in the
|
|
||||||
// requirepass server configuration option (if connecting to a Redis 5.0 instance, or lower),
|
|
||||||
// or the User Password when connecting to a Redis 6.0 instance, or greater,
|
|
||||||
// that is using the Redis ACL system.
|
|
||||||
Password string
|
|
||||||
|
|
||||||
// Database to be selected after connecting to the server.
|
|
||||||
DB int
|
|
||||||
|
|
||||||
// Maximum number of retries before giving up.
|
|
||||||
// Default is 3 retries; -1 (not 0) disables retries.
|
|
||||||
MaxRetries int
|
|
||||||
// Minimum backoff between each retry.
|
|
||||||
// Default is 8 milliseconds; -1 disables backoff.
|
|
||||||
MinRetryBackoff time.Duration
|
|
||||||
// Maximum backoff between each retry.
|
|
||||||
// Default is 512 milliseconds; -1 disables backoff.
|
|
||||||
MaxRetryBackoff time.Duration
|
|
||||||
|
|
||||||
// Dial timeout for establishing new connections.
|
|
||||||
// Default is 5 seconds.
|
|
||||||
DialTimeout time.Duration
|
|
||||||
// Timeout for socket reads. If reached, commands will fail
|
|
||||||
// with a timeout instead of blocking. Use value -1 for no timeout and 0 for default.
|
|
||||||
// Default is 3 seconds.
|
|
||||||
ReadTimeout time.Duration
|
|
||||||
// Timeout for socket writes. If reached, commands will fail
|
|
||||||
// with a timeout instead of blocking.
|
|
||||||
// Default is ReadTimeout.
|
|
||||||
WriteTimeout time.Duration
|
|
||||||
|
|
||||||
// Type of connection pool.
|
|
||||||
// true for FIFO pool, false for LIFO pool.
|
|
||||||
// Note that fifo has higher overhead compared to lifo.
|
|
||||||
PoolFIFO bool
|
|
||||||
// Maximum number of socket connections.
|
|
||||||
// Default is 10 connections per every available CPU as reported by runtime.GOMAXPROCS.
|
|
||||||
PoolSize int
|
|
||||||
// Minimum number of idle connections which is useful when establishing
|
|
||||||
// new connection is slow.
|
|
||||||
MinIdleConns int
|
|
||||||
// Connection age at which client retires (closes) the connection.
|
|
||||||
// Default is to not close aged connections.
|
|
||||||
MaxConnAge time.Duration
|
|
||||||
// Amount of time client waits for connection if all connections
|
|
||||||
// are busy before returning an error.
|
|
||||||
// Default is ReadTimeout + 1 second.
|
|
||||||
PoolTimeout time.Duration
|
|
||||||
// Amount of time after which client closes idle connections.
|
|
||||||
// Should be less than server's timeout.
|
|
||||||
// Default is 5 minutes. -1 disables idle timeout check.
|
|
||||||
IdleTimeout time.Duration
|
|
||||||
// Frequency of idle checks made by idle connections reaper.
|
|
||||||
// Default is 1 minute. -1 disables idle connections reaper,
|
|
||||||
// but idle connections are still discarded by the client
|
|
||||||
// if IdleTimeout is set.
|
|
||||||
IdleCheckFrequency time.Duration
|
|
||||||
|
|
||||||
// Enables read only queries on slave nodes.
|
|
||||||
readOnly bool
|
|
||||||
|
|
||||||
// TLS Config to use. When set TLS will be negotiated.
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
|
|
||||||
// Limiter interface used to implemented circuit breaker or rate limiter.
|
|
||||||
Limiter Limiter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt *Options) init() {
|
|
||||||
if opt.Addr == "" {
|
|
||||||
opt.Addr = "localhost:6379"
|
|
||||||
}
|
|
||||||
if opt.Network == "" {
|
|
||||||
if strings.HasPrefix(opt.Addr, "/") {
|
|
||||||
opt.Network = "unix"
|
|
||||||
} else {
|
|
||||||
opt.Network = "tcp"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if opt.DialTimeout == 0 {
|
|
||||||
opt.DialTimeout = 5 * time.Second
|
|
||||||
}
|
|
||||||
if opt.Dialer == nil {
|
|
||||||
opt.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
netDialer := &net.Dialer{
|
|
||||||
Timeout: opt.DialTimeout,
|
|
||||||
KeepAlive: 5 * time.Minute,
|
|
||||||
}
|
|
||||||
if opt.TLSConfig == nil {
|
|
||||||
return netDialer.DialContext(ctx, network, addr)
|
|
||||||
}
|
|
||||||
return tls.DialWithDialer(netDialer, network, addr, opt.TLSConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if opt.PoolSize == 0 {
|
|
||||||
opt.PoolSize = 10 * runtime.GOMAXPROCS(0)
|
|
||||||
}
|
|
||||||
switch opt.ReadTimeout {
|
|
||||||
case -1:
|
|
||||||
opt.ReadTimeout = 0
|
|
||||||
case 0:
|
|
||||||
opt.ReadTimeout = 3 * time.Second
|
|
||||||
}
|
|
||||||
switch opt.WriteTimeout {
|
|
||||||
case -1:
|
|
||||||
opt.WriteTimeout = 0
|
|
||||||
case 0:
|
|
||||||
opt.WriteTimeout = opt.ReadTimeout
|
|
||||||
}
|
|
||||||
if opt.PoolTimeout == 0 {
|
|
||||||
opt.PoolTimeout = opt.ReadTimeout + time.Second
|
|
||||||
}
|
|
||||||
if opt.IdleTimeout == 0 {
|
|
||||||
opt.IdleTimeout = 5 * time.Minute
|
|
||||||
}
|
|
||||||
if opt.IdleCheckFrequency == 0 {
|
|
||||||
opt.IdleCheckFrequency = time.Minute
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.MaxRetries == -1 {
|
|
||||||
opt.MaxRetries = 0
|
|
||||||
} else if opt.MaxRetries == 0 {
|
|
||||||
opt.MaxRetries = 3
|
|
||||||
}
|
|
||||||
switch opt.MinRetryBackoff {
|
|
||||||
case -1:
|
|
||||||
opt.MinRetryBackoff = 0
|
|
||||||
case 0:
|
|
||||||
opt.MinRetryBackoff = 8 * time.Millisecond
|
|
||||||
}
|
|
||||||
switch opt.MaxRetryBackoff {
|
|
||||||
case -1:
|
|
||||||
opt.MaxRetryBackoff = 0
|
|
||||||
case 0:
|
|
||||||
opt.MaxRetryBackoff = 512 * time.Millisecond
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt *Options) clone() *Options {
|
|
||||||
clone := *opt
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseURL parses an URL into Options that can be used to connect to Redis.
|
|
||||||
// Scheme is required.
|
|
||||||
// There are two connection types: by tcp socket and by unix socket.
|
|
||||||
// Tcp connection:
|
|
||||||
// redis://<user>:<password>@<host>:<port>/<db_number>
|
|
||||||
// Unix connection:
|
|
||||||
// unix://<user>:<password>@</path/to/redis.sock>?db=<db_number>
|
|
||||||
// Most Option fields can be set using query parameters, with the following restrictions:
|
|
||||||
// - field names are mapped using snake-case conversion: to set MaxRetries, use max_retries
|
|
||||||
// - only scalar type fields are supported (bool, int, time.Duration)
|
|
||||||
// - for time.Duration fields, values must be a valid input for time.ParseDuration();
|
|
||||||
// additionally a plain integer as value (i.e. without unit) is intepreted as seconds
|
|
||||||
// - to disable a duration field, use value less than or equal to 0; to use the default
|
|
||||||
// value, leave the value blank or remove the parameter
|
|
||||||
// - only the last value is interpreted if a parameter is given multiple times
|
|
||||||
// - fields "network", "addr", "username" and "password" can only be set using other
|
|
||||||
// URL attributes (scheme, host, userinfo, resp.), query paremeters using these
|
|
||||||
// names will be treated as unknown parameters
|
|
||||||
// - unknown parameter names will result in an error
|
|
||||||
// Examples:
|
|
||||||
// redis://user:password@localhost:6789/3?dial_timeout=3&db=1&read_timeout=6s&max_retries=2
|
|
||||||
// is equivalent to:
|
|
||||||
// &Options{
|
|
||||||
// Network: "tcp",
|
|
||||||
// Addr: "localhost:6789",
|
|
||||||
// DB: 1, // path "/3" was overridden by "&db=1"
|
|
||||||
// DialTimeout: 3 * time.Second, // no time unit = seconds
|
|
||||||
// ReadTimeout: 6 * time.Second,
|
|
||||||
// MaxRetries: 2,
|
|
||||||
// }
|
|
||||||
func ParseURL(redisURL string) (*Options, error) {
|
|
||||||
u, err := url.Parse(redisURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch u.Scheme {
|
|
||||||
case "redis", "rediss":
|
|
||||||
return setupTCPConn(u)
|
|
||||||
case "unix":
|
|
||||||
return setupUnixConn(u)
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("redis: invalid URL scheme: %s", u.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupTCPConn(u *url.URL) (*Options, error) {
|
|
||||||
o := &Options{Network: "tcp"}
|
|
||||||
|
|
||||||
o.Username, o.Password = getUserPassword(u)
|
|
||||||
|
|
||||||
h, p, err := net.SplitHostPort(u.Host)
|
|
||||||
if err != nil {
|
|
||||||
h = u.Host
|
|
||||||
}
|
|
||||||
if h == "" {
|
|
||||||
h = "localhost"
|
|
||||||
}
|
|
||||||
if p == "" {
|
|
||||||
p = "6379"
|
|
||||||
}
|
|
||||||
o.Addr = net.JoinHostPort(h, p)
|
|
||||||
|
|
||||||
f := strings.FieldsFunc(u.Path, func(r rune) bool {
|
|
||||||
return r == '/'
|
|
||||||
})
|
|
||||||
switch len(f) {
|
|
||||||
case 0:
|
|
||||||
o.DB = 0
|
|
||||||
case 1:
|
|
||||||
if o.DB, err = strconv.Atoi(f[0]); err != nil {
|
|
||||||
return nil, fmt.Errorf("redis: invalid database number: %q", f[0])
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("redis: invalid URL path: %s", u.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Scheme == "rediss" {
|
|
||||||
o.TLSConfig = &tls.Config{ServerName: h}
|
|
||||||
}
|
|
||||||
|
|
||||||
return setupConnParams(u, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupUnixConn(u *url.URL) (*Options, error) {
|
|
||||||
o := &Options{
|
|
||||||
Network: "unix",
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.TrimSpace(u.Path) == "" { // path is required with unix connection
|
|
||||||
return nil, errors.New("redis: empty unix socket path")
|
|
||||||
}
|
|
||||||
o.Addr = u.Path
|
|
||||||
o.Username, o.Password = getUserPassword(u)
|
|
||||||
return setupConnParams(u, o)
|
|
||||||
}
|
|
||||||
|
|
||||||
type queryOptions struct {
|
|
||||||
q url.Values
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *queryOptions) string(name string) string {
|
|
||||||
vs := o.q[name]
|
|
||||||
if len(vs) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
delete(o.q, name) // enable detection of unknown parameters
|
|
||||||
return vs[len(vs)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *queryOptions) int(name string) int {
|
|
||||||
s := o.string(name)
|
|
||||||
if s == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
i, err := strconv.Atoi(s)
|
|
||||||
if err == nil {
|
|
||||||
return i
|
|
||||||
}
|
|
||||||
if o.err == nil {
|
|
||||||
o.err = fmt.Errorf("redis: invalid %s number: %s", name, err)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *queryOptions) duration(name string) time.Duration {
|
|
||||||
s := o.string(name)
|
|
||||||
if s == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
// try plain number first
|
|
||||||
if i, err := strconv.Atoi(s); err == nil {
|
|
||||||
if i <= 0 {
|
|
||||||
// disable timeouts
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return time.Duration(i) * time.Second
|
|
||||||
}
|
|
||||||
dur, err := time.ParseDuration(s)
|
|
||||||
if err == nil {
|
|
||||||
return dur
|
|
||||||
}
|
|
||||||
if o.err == nil {
|
|
||||||
o.err = fmt.Errorf("redis: invalid %s duration: %w", name, err)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *queryOptions) bool(name string) bool {
|
|
||||||
switch s := o.string(name); s {
|
|
||||||
case "true", "1":
|
|
||||||
return true
|
|
||||||
case "false", "0", "":
|
|
||||||
return false
|
|
||||||
default:
|
|
||||||
if o.err == nil {
|
|
||||||
o.err = fmt.Errorf("redis: invalid %s boolean: expected true/false/1/0 or an empty string, got %q", name, s)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *queryOptions) remaining() []string {
|
|
||||||
if len(o.q) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
keys := make([]string, 0, len(o.q))
|
|
||||||
for k := range o.q {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// setupConnParams converts query parameters in u to option value in o.
|
|
||||||
func setupConnParams(u *url.URL, o *Options) (*Options, error) {
|
|
||||||
q := queryOptions{q: u.Query()}
|
|
||||||
|
|
||||||
// compat: a future major release may use q.int("db")
|
|
||||||
if tmp := q.string("db"); tmp != "" {
|
|
||||||
db, err := strconv.Atoi(tmp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("redis: invalid database number: %w", err)
|
|
||||||
}
|
|
||||||
o.DB = db
|
|
||||||
}
|
|
||||||
|
|
||||||
o.MaxRetries = q.int("max_retries")
|
|
||||||
o.MinRetryBackoff = q.duration("min_retry_backoff")
|
|
||||||
o.MaxRetryBackoff = q.duration("max_retry_backoff")
|
|
||||||
o.DialTimeout = q.duration("dial_timeout")
|
|
||||||
o.ReadTimeout = q.duration("read_timeout")
|
|
||||||
o.WriteTimeout = q.duration("write_timeout")
|
|
||||||
o.PoolFIFO = q.bool("pool_fifo")
|
|
||||||
o.PoolSize = q.int("pool_size")
|
|
||||||
o.MinIdleConns = q.int("min_idle_conns")
|
|
||||||
o.MaxConnAge = q.duration("max_conn_age")
|
|
||||||
o.PoolTimeout = q.duration("pool_timeout")
|
|
||||||
o.IdleTimeout = q.duration("idle_timeout")
|
|
||||||
o.IdleCheckFrequency = q.duration("idle_check_frequency")
|
|
||||||
if q.err != nil {
|
|
||||||
return nil, q.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// any parameters left?
|
|
||||||
if r := q.remaining(); len(r) > 0 {
|
|
||||||
return nil, fmt.Errorf("redis: unexpected option: %s", strings.Join(r, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
return o, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getUserPassword(u *url.URL) (string, string) {
|
|
||||||
var user, password string
|
|
||||||
if u.User != nil {
|
|
||||||
user = u.User.Username()
|
|
||||||
if p, ok := u.User.Password(); ok {
|
|
||||||
password = p
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return user, password
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConnPool(opt *Options) *pool.ConnPool {
|
|
||||||
return pool.NewConnPool(&pool.Options{
|
|
||||||
Dialer: func(ctx context.Context) (net.Conn, error) {
|
|
||||||
return opt.Dialer(ctx, opt.Network, opt.Addr)
|
|
||||||
},
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
|
||||||
PoolSize: opt.PoolSize,
|
|
||||||
MinIdleConns: opt.MinIdleConns,
|
|
||||||
MaxConnAge: opt.MaxConnAge,
|
|
||||||
PoolTimeout: opt.PoolTimeout,
|
|
||||||
IdleTimeout: opt.IdleTimeout,
|
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "redis",
|
|
||||||
"version": "8.11.5",
|
|
||||||
"main": "index.js",
|
|
||||||
"repository": "git@github.com:go-redis/redis.git",
|
|
||||||
"author": "Vladimir Mihailenco <vladimir.webdev@gmail.com>",
|
|
||||||
"license": "BSD-2-clause"
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
)
|
|
||||||
|
|
||||||
type pipelineExecer func(context.Context, []Cmder) error
|
|
||||||
|
|
||||||
// Pipeliner is an mechanism to realise Redis Pipeline technique.
|
|
||||||
//
|
|
||||||
// Pipelining is a technique to extremely speed up processing by packing
|
|
||||||
// operations to batches, send them at once to Redis and read a replies in a
|
|
||||||
// singe step.
|
|
||||||
// See https://redis.io/topics/pipelining
|
|
||||||
//
|
|
||||||
// Pay attention, that Pipeline is not a transaction, so you can get unexpected
|
|
||||||
// results in case of big pipelines and small read/write timeouts.
|
|
||||||
// Redis client has retransmission logic in case of timeouts, pipeline
|
|
||||||
// can be retransmitted and commands can be executed more then once.
|
|
||||||
// To avoid this: it is good idea to use reasonable bigger read/write timeouts
|
|
||||||
// depends of your batch size and/or use TxPipeline.
|
|
||||||
type Pipeliner interface {
|
|
||||||
StatefulCmdable
|
|
||||||
Len() int
|
|
||||||
Do(ctx context.Context, args ...interface{}) *Cmd
|
|
||||||
Process(ctx context.Context, cmd Cmder) error
|
|
||||||
Close() error
|
|
||||||
Discard() error
|
|
||||||
Exec(ctx context.Context) ([]Cmder, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ Pipeliner = (*Pipeline)(nil)
|
|
||||||
|
|
||||||
// Pipeline implements pipelining as described in
|
|
||||||
// http://redis.io/topics/pipelining. It's safe for concurrent use
|
|
||||||
// by multiple goroutines.
|
|
||||||
type Pipeline struct {
|
|
||||||
cmdable
|
|
||||||
statefulCmdable
|
|
||||||
|
|
||||||
ctx context.Context
|
|
||||||
exec pipelineExecer
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
cmds []Cmder
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Pipeline) init() {
|
|
||||||
c.cmdable = c.Process
|
|
||||||
c.statefulCmdable = c.Process
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the number of queued commands.
|
|
||||||
func (c *Pipeline) Len() int {
|
|
||||||
c.mu.Lock()
|
|
||||||
ln := len(c.cmds)
|
|
||||||
c.mu.Unlock()
|
|
||||||
return ln
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do queues the custom command for later execution.
|
|
||||||
func (c *Pipeline) Do(ctx context.Context, args ...interface{}) *Cmd {
|
|
||||||
cmd := NewCmd(ctx, args...)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process queues the cmd for later execution.
|
|
||||||
func (c *Pipeline) Process(ctx context.Context, cmd Cmder) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.cmds = append(c.cmds, cmd)
|
|
||||||
c.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the pipeline, releasing any open resources.
|
|
||||||
func (c *Pipeline) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
_ = c.discard()
|
|
||||||
c.closed = true
|
|
||||||
c.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Discard resets the pipeline and discards queued commands.
|
|
||||||
func (c *Pipeline) Discard() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
err := c.discard()
|
|
||||||
c.mu.Unlock()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Pipeline) discard() error {
|
|
||||||
if c.closed {
|
|
||||||
return pool.ErrClosed
|
|
||||||
}
|
|
||||||
c.cmds = c.cmds[:0]
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exec executes all previously queued commands using one
|
|
||||||
// client-server roundtrip.
|
|
||||||
//
|
|
||||||
// Exec always returns list of commands and error of the first failed
|
|
||||||
// command if any.
|
|
||||||
func (c *Pipeline) Exec(ctx context.Context) ([]Cmder, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return nil, pool.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.cmds) == 0 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
cmds := c.cmds
|
|
||||||
c.cmds = nil
|
|
||||||
|
|
||||||
return cmds, c.exec(ctx, cmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Pipeline) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
if err := fn(c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
cmds, err := c.Exec(ctx)
|
|
||||||
_ = c.Close()
|
|
||||||
return cmds, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Pipeline) Pipeline() Pipeliner {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Pipeline) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Pipeline) TxPipeline() Pipeliner {
|
|
||||||
return c
|
|
||||||
}
|
|
@ -1,668 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PubSub implements Pub/Sub commands as described in
|
|
||||||
// http://redis.io/topics/pubsub. Message receiving is NOT safe
|
|
||||||
// for concurrent use by multiple goroutines.
|
|
||||||
//
|
|
||||||
// PubSub automatically reconnects to Redis Server and resubscribes
|
|
||||||
// to the channels in case of network errors.
|
|
||||||
type PubSub struct {
|
|
||||||
opt *Options
|
|
||||||
|
|
||||||
newConn func(ctx context.Context, channels []string) (*pool.Conn, error)
|
|
||||||
closeConn func(*pool.Conn) error
|
|
||||||
|
|
||||||
mu sync.Mutex
|
|
||||||
cn *pool.Conn
|
|
||||||
channels map[string]struct{}
|
|
||||||
patterns map[string]struct{}
|
|
||||||
|
|
||||||
closed bool
|
|
||||||
exit chan struct{}
|
|
||||||
|
|
||||||
cmd *Cmd
|
|
||||||
|
|
||||||
chOnce sync.Once
|
|
||||||
msgCh *channel
|
|
||||||
allCh *channel
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) init() {
|
|
||||||
c.exit = make(chan struct{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) String() string {
|
|
||||||
channels := mapKeys(c.channels)
|
|
||||||
channels = append(channels, mapKeys(c.patterns)...)
|
|
||||||
return fmt.Sprintf("PubSub(%s)", strings.Join(channels, ", "))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) connWithLock(ctx context.Context) (*pool.Conn, error) {
|
|
||||||
c.mu.Lock()
|
|
||||||
cn, err := c.conn(ctx, nil)
|
|
||||||
c.mu.Unlock()
|
|
||||||
return cn, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) conn(ctx context.Context, newChannels []string) (*pool.Conn, error) {
|
|
||||||
if c.closed {
|
|
||||||
return nil, pool.ErrClosed
|
|
||||||
}
|
|
||||||
if c.cn != nil {
|
|
||||||
return c.cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
channels := mapKeys(c.channels)
|
|
||||||
channels = append(channels, newChannels...)
|
|
||||||
|
|
||||||
cn, err := c.newConn(ctx, channels)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.resubscribe(ctx, cn); err != nil {
|
|
||||||
_ = c.closeConn(cn)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c.cn = cn
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) writeCmd(ctx context.Context, cn *pool.Conn, cmd Cmder) error {
|
|
||||||
return cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
|
||||||
return writeCmd(wr, cmd)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) resubscribe(ctx context.Context, cn *pool.Conn) error {
|
|
||||||
var firstErr error
|
|
||||||
|
|
||||||
if len(c.channels) > 0 {
|
|
||||||
firstErr = c._subscribe(ctx, cn, "subscribe", mapKeys(c.channels))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(c.patterns) > 0 {
|
|
||||||
err := c._subscribe(ctx, cn, "psubscribe", mapKeys(c.patterns))
|
|
||||||
if err != nil && firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func mapKeys(m map[string]struct{}) []string {
|
|
||||||
s := make([]string, len(m))
|
|
||||||
i := 0
|
|
||||||
for k := range m {
|
|
||||||
s[i] = k
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) _subscribe(
|
|
||||||
ctx context.Context, cn *pool.Conn, redisCmd string, channels []string,
|
|
||||||
) error {
|
|
||||||
args := make([]interface{}, 0, 1+len(channels))
|
|
||||||
args = append(args, redisCmd)
|
|
||||||
for _, channel := range channels {
|
|
||||||
args = append(args, channel)
|
|
||||||
}
|
|
||||||
cmd := NewSliceCmd(ctx, args...)
|
|
||||||
return c.writeCmd(ctx, cn, cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) releaseConnWithLock(
|
|
||||||
ctx context.Context,
|
|
||||||
cn *pool.Conn,
|
|
||||||
err error,
|
|
||||||
allowTimeout bool,
|
|
||||||
) {
|
|
||||||
c.mu.Lock()
|
|
||||||
c.releaseConn(ctx, cn, err, allowTimeout)
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) releaseConn(ctx context.Context, cn *pool.Conn, err error, allowTimeout bool) {
|
|
||||||
if c.cn != cn {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isBadConn(err, allowTimeout, c.opt.Addr) {
|
|
||||||
c.reconnect(ctx, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) reconnect(ctx context.Context, reason error) {
|
|
||||||
_ = c.closeTheCn(reason)
|
|
||||||
_, _ = c.conn(ctx, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) closeTheCn(reason error) error {
|
|
||||||
if c.cn == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !c.closed {
|
|
||||||
internal.Logger.Printf(c.getContext(), "redis: discarding bad PubSub connection: %s", reason)
|
|
||||||
}
|
|
||||||
err := c.closeConn(c.cn)
|
|
||||||
c.cn = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return pool.ErrClosed
|
|
||||||
}
|
|
||||||
c.closed = true
|
|
||||||
close(c.exit)
|
|
||||||
|
|
||||||
return c.closeTheCn(pool.ErrClosed)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe the client to the specified channels. It returns
|
|
||||||
// empty subscription if there are no channels.
|
|
||||||
func (c *PubSub) Subscribe(ctx context.Context, channels ...string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
err := c.subscribe(ctx, "subscribe", channels...)
|
|
||||||
if c.channels == nil {
|
|
||||||
c.channels = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
for _, s := range channels {
|
|
||||||
c.channels[s] = struct{}{}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSubscribe the client to the given patterns. It returns
|
|
||||||
// empty subscription if there are no patterns.
|
|
||||||
func (c *PubSub) PSubscribe(ctx context.Context, patterns ...string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
err := c.subscribe(ctx, "psubscribe", patterns...)
|
|
||||||
if c.patterns == nil {
|
|
||||||
c.patterns = make(map[string]struct{})
|
|
||||||
}
|
|
||||||
for _, s := range patterns {
|
|
||||||
c.patterns[s] = struct{}{}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unsubscribe the client from the given channels, or from all of
|
|
||||||
// them if none is given.
|
|
||||||
func (c *PubSub) Unsubscribe(ctx context.Context, channels ...string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
for _, channel := range channels {
|
|
||||||
delete(c.channels, channel)
|
|
||||||
}
|
|
||||||
err := c.subscribe(ctx, "unsubscribe", channels...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// PUnsubscribe the client from the given patterns, or from all of
|
|
||||||
// them if none is given.
|
|
||||||
func (c *PubSub) PUnsubscribe(ctx context.Context, patterns ...string) error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
for _, pattern := range patterns {
|
|
||||||
delete(c.patterns, pattern)
|
|
||||||
}
|
|
||||||
err := c.subscribe(ctx, "punsubscribe", patterns...)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) subscribe(ctx context.Context, redisCmd string, channels ...string) error {
|
|
||||||
cn, err := c.conn(ctx, channels)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c._subscribe(ctx, cn, redisCmd, channels)
|
|
||||||
c.releaseConn(ctx, cn, err, false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) Ping(ctx context.Context, payload ...string) error {
|
|
||||||
args := []interface{}{"ping"}
|
|
||||||
if len(payload) == 1 {
|
|
||||||
args = append(args, payload[0])
|
|
||||||
}
|
|
||||||
cmd := NewCmd(ctx, args...)
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
cn, err := c.conn(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.writeCmd(ctx, cn, cmd)
|
|
||||||
c.releaseConn(ctx, cn, err, false)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscription received after a successful subscription to channel.
|
|
||||||
type Subscription struct {
|
|
||||||
// Can be "subscribe", "unsubscribe", "psubscribe" or "punsubscribe".
|
|
||||||
Kind string
|
|
||||||
// Channel name we have subscribed to.
|
|
||||||
Channel string
|
|
||||||
// Number of channels we are currently subscribed to.
|
|
||||||
Count int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Subscription) String() string {
|
|
||||||
return fmt.Sprintf("%s: %s", m.Kind, m.Channel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Message received as result of a PUBLISH command issued by another client.
|
|
||||||
type Message struct {
|
|
||||||
Channel string
|
|
||||||
Pattern string
|
|
||||||
Payload string
|
|
||||||
PayloadSlice []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Message) String() string {
|
|
||||||
return fmt.Sprintf("Message<%s: %s>", m.Channel, m.Payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pong received as result of a PING command issued by another client.
|
|
||||||
type Pong struct {
|
|
||||||
Payload string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Pong) String() string {
|
|
||||||
if p.Payload != "" {
|
|
||||||
return fmt.Sprintf("Pong<%s>", p.Payload)
|
|
||||||
}
|
|
||||||
return "Pong"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) newMessage(reply interface{}) (interface{}, error) {
|
|
||||||
switch reply := reply.(type) {
|
|
||||||
case string:
|
|
||||||
return &Pong{
|
|
||||||
Payload: reply,
|
|
||||||
}, nil
|
|
||||||
case []interface{}:
|
|
||||||
switch kind := reply[0].(string); kind {
|
|
||||||
case "subscribe", "unsubscribe", "psubscribe", "punsubscribe":
|
|
||||||
// Can be nil in case of "unsubscribe".
|
|
||||||
channel, _ := reply[1].(string)
|
|
||||||
return &Subscription{
|
|
||||||
Kind: kind,
|
|
||||||
Channel: channel,
|
|
||||||
Count: int(reply[2].(int64)),
|
|
||||||
}, nil
|
|
||||||
case "message":
|
|
||||||
switch payload := reply[2].(type) {
|
|
||||||
case string:
|
|
||||||
return &Message{
|
|
||||||
Channel: reply[1].(string),
|
|
||||||
Payload: payload,
|
|
||||||
}, nil
|
|
||||||
case []interface{}:
|
|
||||||
ss := make([]string, len(payload))
|
|
||||||
for i, s := range payload {
|
|
||||||
ss[i] = s.(string)
|
|
||||||
}
|
|
||||||
return &Message{
|
|
||||||
Channel: reply[1].(string),
|
|
||||||
PayloadSlice: ss,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("redis: unsupported pubsub message payload: %T", payload)
|
|
||||||
}
|
|
||||||
case "pmessage":
|
|
||||||
return &Message{
|
|
||||||
Pattern: reply[1].(string),
|
|
||||||
Channel: reply[2].(string),
|
|
||||||
Payload: reply[3].(string),
|
|
||||||
}, nil
|
|
||||||
case "pong":
|
|
||||||
return &Pong{
|
|
||||||
Payload: reply[1].(string),
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("redis: unsupported pubsub message: %q", kind)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("redis: unsupported pubsub message: %#v", reply)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReceiveTimeout acts like Receive but returns an error if message
|
|
||||||
// is not received in time. This is low-level API and in most cases
|
|
||||||
// Channel should be used instead.
|
|
||||||
func (c *PubSub) ReceiveTimeout(ctx context.Context, timeout time.Duration) (interface{}, error) {
|
|
||||||
if c.cmd == nil {
|
|
||||||
c.cmd = NewCmd(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't hold the lock to allow subscriptions and pings.
|
|
||||||
|
|
||||||
cn, err := c.connWithLock(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cn.WithReader(ctx, timeout, func(rd *proto.Reader) error {
|
|
||||||
return c.cmd.readReply(rd)
|
|
||||||
})
|
|
||||||
|
|
||||||
c.releaseConnWithLock(ctx, cn, err, timeout > 0)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.newMessage(c.cmd.Val())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receive returns a message as a Subscription, Message, Pong or error.
|
|
||||||
// See PubSub example for details. This is low-level API and in most cases
|
|
||||||
// Channel should be used instead.
|
|
||||||
func (c *PubSub) Receive(ctx context.Context) (interface{}, error) {
|
|
||||||
return c.ReceiveTimeout(ctx, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReceiveMessage returns a Message or error ignoring Subscription and Pong
|
|
||||||
// messages. This is low-level API and in most cases Channel should be used
|
|
||||||
// instead.
|
|
||||||
func (c *PubSub) ReceiveMessage(ctx context.Context) (*Message, error) {
|
|
||||||
for {
|
|
||||||
msg, err := c.Receive(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *Subscription:
|
|
||||||
// Ignore.
|
|
||||||
case *Pong:
|
|
||||||
// Ignore.
|
|
||||||
case *Message:
|
|
||||||
return msg, nil
|
|
||||||
default:
|
|
||||||
err := fmt.Errorf("redis: unknown message: %T", msg)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *PubSub) getContext() context.Context {
|
|
||||||
if c.cmd != nil {
|
|
||||||
return c.cmd.ctx
|
|
||||||
}
|
|
||||||
return context.Background()
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Channel returns a Go channel for concurrently receiving messages.
|
|
||||||
// The channel is closed together with the PubSub. If the Go channel
|
|
||||||
// is blocked full for 30 seconds the message is dropped.
|
|
||||||
// Receive* APIs can not be used after channel is created.
|
|
||||||
//
|
|
||||||
// go-redis periodically sends ping messages to test connection health
|
|
||||||
// and re-subscribes if ping can not not received for 30 seconds.
|
|
||||||
func (c *PubSub) Channel(opts ...ChannelOption) <-chan *Message {
|
|
||||||
c.chOnce.Do(func() {
|
|
||||||
c.msgCh = newChannel(c, opts...)
|
|
||||||
c.msgCh.initMsgChan()
|
|
||||||
})
|
|
||||||
if c.msgCh == nil {
|
|
||||||
err := fmt.Errorf("redis: Channel can't be called after ChannelWithSubscriptions")
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return c.msgCh.msgCh
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChannelSize is like Channel, but creates a Go channel
|
|
||||||
// with specified buffer size.
|
|
||||||
//
|
|
||||||
// Deprecated: use Channel(WithChannelSize(size)), remove in v9.
|
|
||||||
func (c *PubSub) ChannelSize(size int) <-chan *Message {
|
|
||||||
return c.Channel(WithChannelSize(size))
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChannelWithSubscriptions is like Channel, but message type can be either
|
|
||||||
// *Subscription or *Message. Subscription messages can be used to detect
|
|
||||||
// reconnections.
|
|
||||||
//
|
|
||||||
// ChannelWithSubscriptions can not be used together with Channel or ChannelSize.
|
|
||||||
func (c *PubSub) ChannelWithSubscriptions(_ context.Context, size int) <-chan interface{} {
|
|
||||||
c.chOnce.Do(func() {
|
|
||||||
c.allCh = newChannel(c, WithChannelSize(size))
|
|
||||||
c.allCh.initAllChan()
|
|
||||||
})
|
|
||||||
if c.allCh == nil {
|
|
||||||
err := fmt.Errorf("redis: ChannelWithSubscriptions can't be called after Channel")
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return c.allCh.allCh
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChannelOption func(c *channel)
|
|
||||||
|
|
||||||
// WithChannelSize specifies the Go chan size that is used to buffer incoming messages.
|
|
||||||
//
|
|
||||||
// The default is 100 messages.
|
|
||||||
func WithChannelSize(size int) ChannelOption {
|
|
||||||
return func(c *channel) {
|
|
||||||
c.chanSize = size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithChannelHealthCheckInterval specifies the health check interval.
|
|
||||||
// PubSub will ping Redis Server if it does not receive any messages within the interval.
|
|
||||||
// To disable health check, use zero interval.
|
|
||||||
//
|
|
||||||
// The default is 3 seconds.
|
|
||||||
func WithChannelHealthCheckInterval(d time.Duration) ChannelOption {
|
|
||||||
return func(c *channel) {
|
|
||||||
c.checkInterval = d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithChannelSendTimeout specifies the channel send timeout after which
|
|
||||||
// the message is dropped.
|
|
||||||
//
|
|
||||||
// The default is 60 seconds.
|
|
||||||
func WithChannelSendTimeout(d time.Duration) ChannelOption {
|
|
||||||
return func(c *channel) {
|
|
||||||
c.chanSendTimeout = d
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type channel struct {
|
|
||||||
pubSub *PubSub
|
|
||||||
|
|
||||||
msgCh chan *Message
|
|
||||||
allCh chan interface{}
|
|
||||||
ping chan struct{}
|
|
||||||
|
|
||||||
chanSize int
|
|
||||||
chanSendTimeout time.Duration
|
|
||||||
checkInterval time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func newChannel(pubSub *PubSub, opts ...ChannelOption) *channel {
|
|
||||||
c := &channel{
|
|
||||||
pubSub: pubSub,
|
|
||||||
|
|
||||||
chanSize: 100,
|
|
||||||
chanSendTimeout: time.Minute,
|
|
||||||
checkInterval: 3 * time.Second,
|
|
||||||
}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(c)
|
|
||||||
}
|
|
||||||
if c.checkInterval > 0 {
|
|
||||||
c.initHealthCheck()
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *channel) initHealthCheck() {
|
|
||||||
ctx := context.TODO()
|
|
||||||
c.ping = make(chan struct{}, 1)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
timer := time.NewTimer(time.Minute)
|
|
||||||
timer.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
timer.Reset(c.checkInterval)
|
|
||||||
select {
|
|
||||||
case <-c.ping:
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
case <-timer.C:
|
|
||||||
if pingErr := c.pubSub.Ping(ctx); pingErr != nil {
|
|
||||||
c.pubSub.mu.Lock()
|
|
||||||
c.pubSub.reconnect(ctx, pingErr)
|
|
||||||
c.pubSub.mu.Unlock()
|
|
||||||
}
|
|
||||||
case <-c.pubSub.exit:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// initMsgChan must be in sync with initAllChan.
|
|
||||||
func (c *channel) initMsgChan() {
|
|
||||||
ctx := context.TODO()
|
|
||||||
c.msgCh = make(chan *Message, c.chanSize)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
timer := time.NewTimer(time.Minute)
|
|
||||||
timer.Stop()
|
|
||||||
|
|
||||||
var errCount int
|
|
||||||
for {
|
|
||||||
msg, err := c.pubSub.Receive(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if err == pool.ErrClosed {
|
|
||||||
close(c.msgCh)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errCount > 0 {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
errCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
errCount = 0
|
|
||||||
|
|
||||||
// Any message is as good as a ping.
|
|
||||||
select {
|
|
||||||
case c.ping <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *Subscription:
|
|
||||||
// Ignore.
|
|
||||||
case *Pong:
|
|
||||||
// Ignore.
|
|
||||||
case *Message:
|
|
||||||
timer.Reset(c.chanSendTimeout)
|
|
||||||
select {
|
|
||||||
case c.msgCh <- msg:
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
case <-timer.C:
|
|
||||||
internal.Logger.Printf(
|
|
||||||
ctx, "redis: %s channel is full for %s (message is dropped)",
|
|
||||||
c, c.chanSendTimeout)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
internal.Logger.Printf(ctx, "redis: unknown message type: %T", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// initAllChan must be in sync with initMsgChan.
|
|
||||||
func (c *channel) initAllChan() {
|
|
||||||
ctx := context.TODO()
|
|
||||||
c.allCh = make(chan interface{}, c.chanSize)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
timer := time.NewTimer(time.Minute)
|
|
||||||
timer.Stop()
|
|
||||||
|
|
||||||
var errCount int
|
|
||||||
for {
|
|
||||||
msg, err := c.pubSub.Receive(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if err == pool.ErrClosed {
|
|
||||||
close(c.allCh)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errCount > 0 {
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
}
|
|
||||||
errCount++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
errCount = 0
|
|
||||||
|
|
||||||
// Any message is as good as a ping.
|
|
||||||
select {
|
|
||||||
case c.ping <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
switch msg := msg.(type) {
|
|
||||||
case *Pong:
|
|
||||||
// Ignore.
|
|
||||||
case *Subscription, *Message:
|
|
||||||
timer.Reset(c.chanSendTimeout)
|
|
||||||
select {
|
|
||||||
case c.allCh <- msg:
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
case <-timer.C:
|
|
||||||
internal.Logger.Printf(
|
|
||||||
ctx, "redis: %s channel is full for %s (message is dropped)",
|
|
||||||
c, c.chanSendTimeout)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
internal.Logger.Printf(ctx, "redis: unknown message type: %T", msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
@ -1,773 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Nil reply returned by Redis when key does not exist.
|
|
||||||
const Nil = proto.Nil
|
|
||||||
|
|
||||||
func SetLogger(logger internal.Logging) {
|
|
||||||
internal.Logger = logger
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type Hook interface {
|
|
||||||
BeforeProcess(ctx context.Context, cmd Cmder) (context.Context, error)
|
|
||||||
AfterProcess(ctx context.Context, cmd Cmder) error
|
|
||||||
|
|
||||||
BeforeProcessPipeline(ctx context.Context, cmds []Cmder) (context.Context, error)
|
|
||||||
AfterProcessPipeline(ctx context.Context, cmds []Cmder) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type hooks struct {
|
|
||||||
hooks []Hook
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs *hooks) lock() {
|
|
||||||
hs.hooks = hs.hooks[:len(hs.hooks):len(hs.hooks)]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs hooks) clone() hooks {
|
|
||||||
clone := hs
|
|
||||||
clone.lock()
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs *hooks) AddHook(hook Hook) {
|
|
||||||
hs.hooks = append(hs.hooks, hook)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs hooks) process(
|
|
||||||
ctx context.Context, cmd Cmder, fn func(context.Context, Cmder) error,
|
|
||||||
) error {
|
|
||||||
if len(hs.hooks) == 0 {
|
|
||||||
err := fn(ctx, cmd)
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var hookIndex int
|
|
||||||
var retErr error
|
|
||||||
|
|
||||||
for ; hookIndex < len(hs.hooks) && retErr == nil; hookIndex++ {
|
|
||||||
ctx, retErr = hs.hooks[hookIndex].BeforeProcess(ctx, cmd)
|
|
||||||
if retErr != nil {
|
|
||||||
cmd.SetErr(retErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if retErr == nil {
|
|
||||||
retErr = fn(ctx, cmd)
|
|
||||||
cmd.SetErr(retErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
for hookIndex--; hookIndex >= 0; hookIndex-- {
|
|
||||||
if err := hs.hooks[hookIndex].AfterProcess(ctx, cmd); err != nil {
|
|
||||||
retErr = err
|
|
||||||
cmd.SetErr(retErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs hooks) processPipeline(
|
|
||||||
ctx context.Context, cmds []Cmder, fn func(context.Context, []Cmder) error,
|
|
||||||
) error {
|
|
||||||
if len(hs.hooks) == 0 {
|
|
||||||
err := fn(ctx, cmds)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var hookIndex int
|
|
||||||
var retErr error
|
|
||||||
|
|
||||||
for ; hookIndex < len(hs.hooks) && retErr == nil; hookIndex++ {
|
|
||||||
ctx, retErr = hs.hooks[hookIndex].BeforeProcessPipeline(ctx, cmds)
|
|
||||||
if retErr != nil {
|
|
||||||
setCmdsErr(cmds, retErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if retErr == nil {
|
|
||||||
retErr = fn(ctx, cmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
for hookIndex--; hookIndex >= 0; hookIndex-- {
|
|
||||||
if err := hs.hooks[hookIndex].AfterProcessPipeline(ctx, cmds); err != nil {
|
|
||||||
retErr = err
|
|
||||||
setCmdsErr(cmds, retErr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return retErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hs hooks) processTxPipeline(
|
|
||||||
ctx context.Context, cmds []Cmder, fn func(context.Context, []Cmder) error,
|
|
||||||
) error {
|
|
||||||
cmds = wrapMultiExec(ctx, cmds)
|
|
||||||
return hs.processPipeline(ctx, cmds, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type baseClient struct {
|
|
||||||
opt *Options
|
|
||||||
connPool pool.Pooler
|
|
||||||
|
|
||||||
onClose func() error // hook called when client is closed
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBaseClient(opt *Options, connPool pool.Pooler) *baseClient {
|
|
||||||
return &baseClient{
|
|
||||||
opt: opt,
|
|
||||||
connPool: connPool,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) clone() *baseClient {
|
|
||||||
clone := *c
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) withTimeout(timeout time.Duration) *baseClient {
|
|
||||||
opt := c.opt.clone()
|
|
||||||
opt.ReadTimeout = timeout
|
|
||||||
opt.WriteTimeout = timeout
|
|
||||||
|
|
||||||
clone := c.clone()
|
|
||||||
clone.opt = opt
|
|
||||||
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) String() string {
|
|
||||||
return fmt.Sprintf("Redis<%s db:%d>", c.getAddr(), c.opt.DB)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) newConn(ctx context.Context) (*pool.Conn, error) {
|
|
||||||
cn, err := c.connPool.NewConn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = c.initConn(ctx, cn)
|
|
||||||
if err != nil {
|
|
||||||
_ = c.connPool.CloseConn(cn)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) getConn(ctx context.Context) (*pool.Conn, error) {
|
|
||||||
if c.opt.Limiter != nil {
|
|
||||||
err := c.opt.Limiter.Allow()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cn, err := c._getConn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
if c.opt.Limiter != nil {
|
|
||||||
c.opt.Limiter.ReportResult(err)
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) _getConn(ctx context.Context) (*pool.Conn, error) {
|
|
||||||
cn, err := c.connPool.Get(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if cn.Inited {
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.initConn(ctx, cn); err != nil {
|
|
||||||
c.connPool.Remove(ctx, cn, err)
|
|
||||||
if err := errors.Unwrap(err); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return cn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) initConn(ctx context.Context, cn *pool.Conn) error {
|
|
||||||
if cn.Inited {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cn.Inited = true
|
|
||||||
|
|
||||||
if c.opt.Password == "" &&
|
|
||||||
c.opt.DB == 0 &&
|
|
||||||
!c.opt.readOnly &&
|
|
||||||
c.opt.OnConnect == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
connPool := pool.NewSingleConnPool(c.connPool, cn)
|
|
||||||
conn := newConn(ctx, c.opt, connPool)
|
|
||||||
|
|
||||||
_, err := conn.Pipelined(ctx, func(pipe Pipeliner) error {
|
|
||||||
if c.opt.Password != "" {
|
|
||||||
if c.opt.Username != "" {
|
|
||||||
pipe.AuthACL(ctx, c.opt.Username, c.opt.Password)
|
|
||||||
} else {
|
|
||||||
pipe.Auth(ctx, c.opt.Password)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.opt.DB > 0 {
|
|
||||||
pipe.Select(ctx, c.opt.DB)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.opt.readOnly {
|
|
||||||
pipe.ReadOnly(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.opt.OnConnect != nil {
|
|
||||||
return c.opt.OnConnect(ctx, conn)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) releaseConn(ctx context.Context, cn *pool.Conn, err error) {
|
|
||||||
if c.opt.Limiter != nil {
|
|
||||||
c.opt.Limiter.ReportResult(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if isBadConn(err, false, c.opt.Addr) {
|
|
||||||
c.connPool.Remove(ctx, cn, err)
|
|
||||||
} else {
|
|
||||||
c.connPool.Put(ctx, cn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) withConn(
|
|
||||||
ctx context.Context, fn func(context.Context, *pool.Conn) error,
|
|
||||||
) error {
|
|
||||||
cn, err := c.getConn(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
c.releaseConn(ctx, cn, err)
|
|
||||||
}()
|
|
||||||
|
|
||||||
done := ctx.Done() //nolint:ifshort
|
|
||||||
|
|
||||||
if done == nil {
|
|
||||||
err = fn(ctx, cn)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
errc := make(chan error, 1)
|
|
||||||
go func() { errc <- fn(ctx, cn) }()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
_ = cn.Close()
|
|
||||||
// Wait for the goroutine to finish and send something.
|
|
||||||
<-errc
|
|
||||||
|
|
||||||
err = ctx.Err()
|
|
||||||
return err
|
|
||||||
case err = <-errc:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) process(ctx context.Context, cmd Cmder) error {
|
|
||||||
var lastErr error
|
|
||||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
|
|
||||||
attempt := attempt
|
|
||||||
|
|
||||||
retry, err := c._process(ctx, cmd, attempt)
|
|
||||||
if err == nil || !retry {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = err
|
|
||||||
}
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) _process(ctx context.Context, cmd Cmder, attempt int) (bool, error) {
|
|
||||||
if attempt > 0 {
|
|
||||||
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
retryTimeout := uint32(1)
|
|
||||||
err := c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
|
||||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
|
||||||
return writeCmd(wr, cmd)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cn.WithReader(ctx, c.cmdTimeout(cmd), cmd.readReply)
|
|
||||||
if err != nil {
|
|
||||||
if cmd.readTimeout() == nil {
|
|
||||||
atomic.StoreUint32(&retryTimeout, 1)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
retry := shouldRetry(err, atomic.LoadUint32(&retryTimeout) == 1)
|
|
||||||
return retry, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) retryBackoff(attempt int) time.Duration {
|
|
||||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) cmdTimeout(cmd Cmder) time.Duration {
|
|
||||||
if timeout := cmd.readTimeout(); timeout != nil {
|
|
||||||
t := *timeout
|
|
||||||
if t == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return t + 10*time.Second
|
|
||||||
}
|
|
||||||
return c.opt.ReadTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the client, releasing any open resources.
|
|
||||||
//
|
|
||||||
// It is rare to Close a Client, as the Client is meant to be
|
|
||||||
// long-lived and shared between many goroutines.
|
|
||||||
func (c *baseClient) Close() error {
|
|
||||||
var firstErr error
|
|
||||||
if c.onClose != nil {
|
|
||||||
if err := c.onClose(); err != nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := c.connPool.Close(); err != nil && firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) getAddr() string {
|
|
||||||
return c.opt.Addr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) processPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.generalProcessPipeline(ctx, cmds, c.pipelineProcessCmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.generalProcessPipeline(ctx, cmds, c.txPipelineProcessCmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
type pipelineProcessor func(context.Context, *pool.Conn, []Cmder) (bool, error)
|
|
||||||
|
|
||||||
func (c *baseClient) generalProcessPipeline(
|
|
||||||
ctx context.Context, cmds []Cmder, p pipelineProcessor,
|
|
||||||
) error {
|
|
||||||
err := c._generalProcessPipeline(ctx, cmds, p)
|
|
||||||
if err != nil {
|
|
||||||
setCmdsErr(cmds, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return cmdsFirstErr(cmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) _generalProcessPipeline(
|
|
||||||
ctx context.Context, cmds []Cmder, p pipelineProcessor,
|
|
||||||
) error {
|
|
||||||
var lastErr error
|
|
||||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
|
|
||||||
if attempt > 0 {
|
|
||||||
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var canRetry bool
|
|
||||||
lastErr = c.withConn(ctx, func(ctx context.Context, cn *pool.Conn) error {
|
|
||||||
var err error
|
|
||||||
canRetry, err = p(ctx, cn, cmds)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
if lastErr == nil || !canRetry || !shouldRetry(lastErr, true) {
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) pipelineProcessCmds(
|
|
||||||
ctx context.Context, cn *pool.Conn, cmds []Cmder,
|
|
||||||
) (bool, error) {
|
|
||||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
|
||||||
return writeCmds(wr, cmds)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
|
||||||
return pipelineReadCmds(rd, cmds)
|
|
||||||
})
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func pipelineReadCmds(rd *proto.Reader, cmds []Cmder) error {
|
|
||||||
for _, cmd := range cmds {
|
|
||||||
err := cmd.readReply(rd)
|
|
||||||
cmd.SetErr(err)
|
|
||||||
if err != nil && !isRedisError(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *baseClient) txPipelineProcessCmds(
|
|
||||||
ctx context.Context, cn *pool.Conn, cmds []Cmder,
|
|
||||||
) (bool, error) {
|
|
||||||
err := cn.WithWriter(ctx, c.opt.WriteTimeout, func(wr *proto.Writer) error {
|
|
||||||
return writeCmds(wr, cmds)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = cn.WithReader(ctx, c.opt.ReadTimeout, func(rd *proto.Reader) error {
|
|
||||||
statusCmd := cmds[0].(*StatusCmd)
|
|
||||||
// Trim multi and exec.
|
|
||||||
cmds = cmds[1 : len(cmds)-1]
|
|
||||||
|
|
||||||
err := txPipelineReadQueued(rd, statusCmd, cmds)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return pipelineReadCmds(rd, cmds)
|
|
||||||
})
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapMultiExec(ctx context.Context, cmds []Cmder) []Cmder {
|
|
||||||
if len(cmds) == 0 {
|
|
||||||
panic("not reached")
|
|
||||||
}
|
|
||||||
cmdCopy := make([]Cmder, len(cmds)+2)
|
|
||||||
cmdCopy[0] = NewStatusCmd(ctx, "multi")
|
|
||||||
copy(cmdCopy[1:], cmds)
|
|
||||||
cmdCopy[len(cmdCopy)-1] = NewSliceCmd(ctx, "exec")
|
|
||||||
return cmdCopy
|
|
||||||
}
|
|
||||||
|
|
||||||
func txPipelineReadQueued(rd *proto.Reader, statusCmd *StatusCmd, cmds []Cmder) error {
|
|
||||||
// Parse queued replies.
|
|
||||||
if err := statusCmd.readReply(rd); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for range cmds {
|
|
||||||
if err := statusCmd.readReply(rd); err != nil && !isRedisError(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse number of replies.
|
|
||||||
line, err := rd.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
if err == Nil {
|
|
||||||
err = TxFailedErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch line[0] {
|
|
||||||
case proto.ErrorReply:
|
|
||||||
return proto.ParseErrorReply(line)
|
|
||||||
case proto.ArrayReply:
|
|
||||||
// ok
|
|
||||||
default:
|
|
||||||
err := fmt.Errorf("redis: expected '*', but got line %q", line)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Client is a Redis client representing a pool of zero or more
|
|
||||||
// underlying connections. It's safe for concurrent use by multiple
|
|
||||||
// goroutines.
|
|
||||||
type Client struct {
|
|
||||||
*baseClient
|
|
||||||
cmdable
|
|
||||||
hooks
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClient returns a client to the Redis Server specified by Options.
|
|
||||||
func NewClient(opt *Options) *Client {
|
|
||||||
opt.init()
|
|
||||||
|
|
||||||
c := Client{
|
|
||||||
baseClient: newBaseClient(opt, newConnPool(opt)),
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
|
||||||
c.cmdable = c.Process
|
|
||||||
|
|
||||||
return &c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) clone() *Client {
|
|
||||||
clone := *c
|
|
||||||
clone.cmdable = clone.Process
|
|
||||||
clone.hooks.lock()
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) WithTimeout(timeout time.Duration) *Client {
|
|
||||||
clone := c.clone()
|
|
||||||
clone.baseClient = c.baseClient.withTimeout(timeout)
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Context() context.Context {
|
|
||||||
return c.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) WithContext(ctx context.Context) *Client {
|
|
||||||
if ctx == nil {
|
|
||||||
panic("nil context")
|
|
||||||
}
|
|
||||||
clone := c.clone()
|
|
||||||
clone.ctx = ctx
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Conn(ctx context.Context) *Conn {
|
|
||||||
return newConn(ctx, c.opt, pool.NewStickyConnPool(c.connPool))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do creates a Cmd from the args and processes the cmd.
|
|
||||||
func (c *Client) Do(ctx context.Context, args ...interface{}) *Cmd {
|
|
||||||
cmd := NewCmd(ctx, args...)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Process(ctx context.Context, cmd Cmder) error {
|
|
||||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) processPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processPipeline(ctx, cmds, c.baseClient.processPipeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processTxPipeline(ctx, cmds, c.baseClient.processTxPipeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options returns read-only Options that were used to create the client.
|
|
||||||
func (c *Client) Options() *Options {
|
|
||||||
return c.opt
|
|
||||||
}
|
|
||||||
|
|
||||||
type PoolStats pool.Stats
|
|
||||||
|
|
||||||
// PoolStats returns connection pool stats.
|
|
||||||
func (c *Client) PoolStats() *PoolStats {
|
|
||||||
stats := c.connPool.Stats()
|
|
||||||
return (*PoolStats)(stats)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.Pipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Pipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: c.processPipeline,
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.TxPipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
|
||||||
func (c *Client) TxPipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: c.processTxPipeline,
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) pubSub() *PubSub {
|
|
||||||
pubsub := &PubSub{
|
|
||||||
opt: c.opt,
|
|
||||||
|
|
||||||
newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) {
|
|
||||||
return c.newConn(ctx)
|
|
||||||
},
|
|
||||||
closeConn: c.connPool.CloseConn,
|
|
||||||
}
|
|
||||||
pubsub.init()
|
|
||||||
return pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe subscribes the client to the specified channels.
|
|
||||||
// Channels can be omitted to create empty subscription.
|
|
||||||
// Note that this method does not wait on a response from Redis, so the
|
|
||||||
// subscription may not be active immediately. To force the connection to wait,
|
|
||||||
// you may call the Receive() method on the returned *PubSub like so:
|
|
||||||
//
|
|
||||||
// sub := client.Subscribe(queryResp)
|
|
||||||
// iface, err := sub.Receive()
|
|
||||||
// if err != nil {
|
|
||||||
// // handle error
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // Should be *Subscription, but others are possible if other actions have been
|
|
||||||
// // taken on sub since it was created.
|
|
||||||
// switch iface.(type) {
|
|
||||||
// case *Subscription:
|
|
||||||
// // subscribe succeeded
|
|
||||||
// case *Message:
|
|
||||||
// // received first message
|
|
||||||
// case *Pong:
|
|
||||||
// // pong received
|
|
||||||
// default:
|
|
||||||
// // handle error
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// ch := sub.Channel()
|
|
||||||
func (c *Client) Subscribe(ctx context.Context, channels ...string) *PubSub {
|
|
||||||
pubsub := c.pubSub()
|
|
||||||
if len(channels) > 0 {
|
|
||||||
_ = pubsub.Subscribe(ctx, channels...)
|
|
||||||
}
|
|
||||||
return pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSubscribe subscribes the client to the given patterns.
|
|
||||||
// Patterns can be omitted to create empty subscription.
|
|
||||||
func (c *Client) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
|
||||||
pubsub := c.pubSub()
|
|
||||||
if len(channels) > 0 {
|
|
||||||
_ = pubsub.PSubscribe(ctx, channels...)
|
|
||||||
}
|
|
||||||
return pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type conn struct {
|
|
||||||
baseClient
|
|
||||||
cmdable
|
|
||||||
statefulCmdable
|
|
||||||
hooks // TODO: inherit hooks
|
|
||||||
}
|
|
||||||
|
|
||||||
// Conn represents a single Redis connection rather than a pool of connections.
|
|
||||||
// Prefer running commands from Client unless there is a specific need
|
|
||||||
// for a continuous single Redis connection.
|
|
||||||
type Conn struct {
|
|
||||||
*conn
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func newConn(ctx context.Context, opt *Options, connPool pool.Pooler) *Conn {
|
|
||||||
c := Conn{
|
|
||||||
conn: &conn{
|
|
||||||
baseClient: baseClient{
|
|
||||||
opt: opt,
|
|
||||||
connPool: connPool,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
c.cmdable = c.Process
|
|
||||||
c.statefulCmdable = c.Process
|
|
||||||
return &c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Process(ctx context.Context, cmd Cmder) error {
|
|
||||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) processPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processPipeline(ctx, cmds, c.baseClient.processPipeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processTxPipeline(ctx, cmds, c.baseClient.processTxPipeline)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.Pipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) Pipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: c.processPipeline,
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Conn) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.TxPipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxPipeline acts like Pipeline, but wraps queued commands with MULTI/EXEC.
|
|
||||||
func (c *Conn) TxPipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: c.processTxPipeline,
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
@ -1,180 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
// NewCmdResult returns a Cmd initialised with val and err for testing.
|
|
||||||
func NewCmdResult(val interface{}, err error) *Cmd {
|
|
||||||
var cmd Cmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSliceResult returns a SliceCmd initialised with val and err for testing.
|
|
||||||
func NewSliceResult(val []interface{}, err error) *SliceCmd {
|
|
||||||
var cmd SliceCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStatusResult returns a StatusCmd initialised with val and err for testing.
|
|
||||||
func NewStatusResult(val string, err error) *StatusCmd {
|
|
||||||
var cmd StatusCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewIntResult returns an IntCmd initialised with val and err for testing.
|
|
||||||
func NewIntResult(val int64, err error) *IntCmd {
|
|
||||||
var cmd IntCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDurationResult returns a DurationCmd initialised with val and err for testing.
|
|
||||||
func NewDurationResult(val time.Duration, err error) *DurationCmd {
|
|
||||||
var cmd DurationCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBoolResult returns a BoolCmd initialised with val and err for testing.
|
|
||||||
func NewBoolResult(val bool, err error) *BoolCmd {
|
|
||||||
var cmd BoolCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStringResult returns a StringCmd initialised with val and err for testing.
|
|
||||||
func NewStringResult(val string, err error) *StringCmd {
|
|
||||||
var cmd StringCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFloatResult returns a FloatCmd initialised with val and err for testing.
|
|
||||||
func NewFloatResult(val float64, err error) *FloatCmd {
|
|
||||||
var cmd FloatCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStringSliceResult returns a StringSliceCmd initialised with val and err for testing.
|
|
||||||
func NewStringSliceResult(val []string, err error) *StringSliceCmd {
|
|
||||||
var cmd StringSliceCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBoolSliceResult returns a BoolSliceCmd initialised with val and err for testing.
|
|
||||||
func NewBoolSliceResult(val []bool, err error) *BoolSliceCmd {
|
|
||||||
var cmd BoolSliceCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStringStringMapResult returns a StringStringMapCmd initialised with val and err for testing.
|
|
||||||
func NewStringStringMapResult(val map[string]string, err error) *StringStringMapCmd {
|
|
||||||
var cmd StringStringMapCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStringIntMapCmdResult returns a StringIntMapCmd initialised with val and err for testing.
|
|
||||||
func NewStringIntMapCmdResult(val map[string]int64, err error) *StringIntMapCmd {
|
|
||||||
var cmd StringIntMapCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTimeCmdResult returns a TimeCmd initialised with val and err for testing.
|
|
||||||
func NewTimeCmdResult(val time.Time, err error) *TimeCmd {
|
|
||||||
var cmd TimeCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewZSliceCmdResult returns a ZSliceCmd initialised with val and err for testing.
|
|
||||||
func NewZSliceCmdResult(val []Z, err error) *ZSliceCmd {
|
|
||||||
var cmd ZSliceCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewZWithKeyCmdResult returns a NewZWithKeyCmd initialised with val and err for testing.
|
|
||||||
func NewZWithKeyCmdResult(val *ZWithKey, err error) *ZWithKeyCmd {
|
|
||||||
var cmd ZWithKeyCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewScanCmdResult returns a ScanCmd initialised with val and err for testing.
|
|
||||||
func NewScanCmdResult(keys []string, cursor uint64, err error) *ScanCmd {
|
|
||||||
var cmd ScanCmd
|
|
||||||
cmd.page = keys
|
|
||||||
cmd.cursor = cursor
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewClusterSlotsCmdResult returns a ClusterSlotsCmd initialised with val and err for testing.
|
|
||||||
func NewClusterSlotsCmdResult(val []ClusterSlot, err error) *ClusterSlotsCmd {
|
|
||||||
var cmd ClusterSlotsCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGeoLocationCmdResult returns a GeoLocationCmd initialised with val and err for testing.
|
|
||||||
func NewGeoLocationCmdResult(val []GeoLocation, err error) *GeoLocationCmd {
|
|
||||||
var cmd GeoLocationCmd
|
|
||||||
cmd.locations = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewGeoPosCmdResult returns a GeoPosCmd initialised with val and err for testing.
|
|
||||||
func NewGeoPosCmdResult(val []*GeoPos, err error) *GeoPosCmd {
|
|
||||||
var cmd GeoPosCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCommandsInfoCmdResult returns a CommandsInfoCmd initialised with val and err for testing.
|
|
||||||
func NewCommandsInfoCmdResult(val map[string]*CommandInfo, err error) *CommandsInfoCmd {
|
|
||||||
var cmd CommandsInfoCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewXMessageSliceCmdResult returns a XMessageSliceCmd initialised with val and err for testing.
|
|
||||||
func NewXMessageSliceCmdResult(val []XMessage, err error) *XMessageSliceCmd {
|
|
||||||
var cmd XMessageSliceCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewXStreamSliceCmdResult returns a XStreamSliceCmd initialised with val and err for testing.
|
|
||||||
func NewXStreamSliceCmdResult(val []XStream, err error) *XStreamSliceCmd {
|
|
||||||
var cmd XStreamSliceCmd
|
|
||||||
cmd.val = val
|
|
||||||
cmd.SetErr(err)
|
|
||||||
return &cmd
|
|
||||||
}
|
|
@ -1,736 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cespare/xxhash/v2"
|
|
||||||
rendezvous "github.com/dgryski/go-rendezvous" //nolint
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
|
||||||
"github.com/go-redis/redis/v8/internal/hashtag"
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errRingShardsDown = errors.New("redis: all ring shards are down")
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type ConsistentHash interface {
|
|
||||||
Get(string) string
|
|
||||||
}
|
|
||||||
|
|
||||||
type rendezvousWrapper struct {
|
|
||||||
*rendezvous.Rendezvous
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w rendezvousWrapper) Get(key string) string {
|
|
||||||
return w.Lookup(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRendezvous(shards []string) ConsistentHash {
|
|
||||||
return rendezvousWrapper{rendezvous.New(shards, xxhash.Sum64String)}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// RingOptions are used to configure a ring client and should be
|
|
||||||
// passed to NewRing.
|
|
||||||
type RingOptions struct {
|
|
||||||
// Map of name => host:port addresses of ring shards.
|
|
||||||
Addrs map[string]string
|
|
||||||
|
|
||||||
// NewClient creates a shard client with provided name and options.
|
|
||||||
NewClient func(name string, opt *Options) *Client
|
|
||||||
|
|
||||||
// Frequency of PING commands sent to check shards availability.
|
|
||||||
// Shard is considered down after 3 subsequent failed checks.
|
|
||||||
HeartbeatFrequency time.Duration
|
|
||||||
|
|
||||||
// NewConsistentHash returns a consistent hash that is used
|
|
||||||
// to distribute keys across the shards.
|
|
||||||
//
|
|
||||||
// See https://medium.com/@dgryski/consistent-hashing-algorithmic-tradeoffs-ef6b8e2fcae8
|
|
||||||
// for consistent hashing algorithmic tradeoffs.
|
|
||||||
NewConsistentHash func(shards []string) ConsistentHash
|
|
||||||
|
|
||||||
// Following options are copied from Options struct.
|
|
||||||
|
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
|
||||||
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
DB int
|
|
||||||
|
|
||||||
MaxRetries int
|
|
||||||
MinRetryBackoff time.Duration
|
|
||||||
MaxRetryBackoff time.Duration
|
|
||||||
|
|
||||||
DialTimeout time.Duration
|
|
||||||
ReadTimeout time.Duration
|
|
||||||
WriteTimeout time.Duration
|
|
||||||
|
|
||||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
|
||||||
PoolFIFO bool
|
|
||||||
|
|
||||||
PoolSize int
|
|
||||||
MinIdleConns int
|
|
||||||
MaxConnAge time.Duration
|
|
||||||
PoolTimeout time.Duration
|
|
||||||
IdleTimeout time.Duration
|
|
||||||
IdleCheckFrequency time.Duration
|
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
Limiter Limiter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt *RingOptions) init() {
|
|
||||||
if opt.NewClient == nil {
|
|
||||||
opt.NewClient = func(name string, opt *Options) *Client {
|
|
||||||
return NewClient(opt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.HeartbeatFrequency == 0 {
|
|
||||||
opt.HeartbeatFrequency = 500 * time.Millisecond
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.NewConsistentHash == nil {
|
|
||||||
opt.NewConsistentHash = newRendezvous
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.MaxRetries == -1 {
|
|
||||||
opt.MaxRetries = 0
|
|
||||||
} else if opt.MaxRetries == 0 {
|
|
||||||
opt.MaxRetries = 3
|
|
||||||
}
|
|
||||||
switch opt.MinRetryBackoff {
|
|
||||||
case -1:
|
|
||||||
opt.MinRetryBackoff = 0
|
|
||||||
case 0:
|
|
||||||
opt.MinRetryBackoff = 8 * time.Millisecond
|
|
||||||
}
|
|
||||||
switch opt.MaxRetryBackoff {
|
|
||||||
case -1:
|
|
||||||
opt.MaxRetryBackoff = 0
|
|
||||||
case 0:
|
|
||||||
opt.MaxRetryBackoff = 512 * time.Millisecond
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt *RingOptions) clientOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
Dialer: opt.Dialer,
|
|
||||||
OnConnect: opt.OnConnect,
|
|
||||||
|
|
||||||
Username: opt.Username,
|
|
||||||
Password: opt.Password,
|
|
||||||
DB: opt.DB,
|
|
||||||
|
|
||||||
MaxRetries: -1,
|
|
||||||
|
|
||||||
DialTimeout: opt.DialTimeout,
|
|
||||||
ReadTimeout: opt.ReadTimeout,
|
|
||||||
WriteTimeout: opt.WriteTimeout,
|
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
|
||||||
PoolSize: opt.PoolSize,
|
|
||||||
MinIdleConns: opt.MinIdleConns,
|
|
||||||
MaxConnAge: opt.MaxConnAge,
|
|
||||||
PoolTimeout: opt.PoolTimeout,
|
|
||||||
IdleTimeout: opt.IdleTimeout,
|
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
|
||||||
Limiter: opt.Limiter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type ringShard struct {
|
|
||||||
Client *Client
|
|
||||||
down int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRingShard(opt *RingOptions, name, addr string) *ringShard {
|
|
||||||
clopt := opt.clientOptions()
|
|
||||||
clopt.Addr = addr
|
|
||||||
|
|
||||||
return &ringShard{
|
|
||||||
Client: opt.NewClient(name, clopt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (shard *ringShard) String() string {
|
|
||||||
var state string
|
|
||||||
if shard.IsUp() {
|
|
||||||
state = "up"
|
|
||||||
} else {
|
|
||||||
state = "down"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s is %s", shard.Client, state)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (shard *ringShard) IsDown() bool {
|
|
||||||
const threshold = 3
|
|
||||||
return atomic.LoadInt32(&shard.down) >= threshold
|
|
||||||
}
|
|
||||||
|
|
||||||
func (shard *ringShard) IsUp() bool {
|
|
||||||
return !shard.IsDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vote votes to set shard state and returns true if state was changed.
|
|
||||||
func (shard *ringShard) Vote(up bool) bool {
|
|
||||||
if up {
|
|
||||||
changed := shard.IsDown()
|
|
||||||
atomic.StoreInt32(&shard.down, 0)
|
|
||||||
return changed
|
|
||||||
}
|
|
||||||
|
|
||||||
if shard.IsDown() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic.AddInt32(&shard.down, 1)
|
|
||||||
return shard.IsDown()
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type ringShards struct {
|
|
||||||
opt *RingOptions
|
|
||||||
|
|
||||||
mu sync.RWMutex
|
|
||||||
hash ConsistentHash
|
|
||||||
shards map[string]*ringShard // read only
|
|
||||||
list []*ringShard // read only
|
|
||||||
numShard int
|
|
||||||
closed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRingShards(opt *RingOptions) *ringShards {
|
|
||||||
shards := make(map[string]*ringShard, len(opt.Addrs))
|
|
||||||
list := make([]*ringShard, 0, len(shards))
|
|
||||||
|
|
||||||
for name, addr := range opt.Addrs {
|
|
||||||
shard := newRingShard(opt, name, addr)
|
|
||||||
shards[name] = shard
|
|
||||||
|
|
||||||
list = append(list, shard)
|
|
||||||
}
|
|
||||||
|
|
||||||
c := &ringShards{
|
|
||||||
opt: opt,
|
|
||||||
|
|
||||||
shards: shards,
|
|
||||||
list: list,
|
|
||||||
}
|
|
||||||
c.rebalance()
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ringShards) List() []*ringShard {
|
|
||||||
var list []*ringShard
|
|
||||||
|
|
||||||
c.mu.RLock()
|
|
||||||
if !c.closed {
|
|
||||||
list = c.list
|
|
||||||
}
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ringShards) Hash(key string) string {
|
|
||||||
key = hashtag.Key(key)
|
|
||||||
|
|
||||||
var hash string
|
|
||||||
|
|
||||||
c.mu.RLock()
|
|
||||||
if c.numShard > 0 {
|
|
||||||
hash = c.hash.Get(key)
|
|
||||||
}
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
return hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ringShards) GetByKey(key string) (*ringShard, error) {
|
|
||||||
key = hashtag.Key(key)
|
|
||||||
|
|
||||||
c.mu.RLock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
c.mu.RUnlock()
|
|
||||||
return nil, pool.ErrClosed
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.numShard == 0 {
|
|
||||||
c.mu.RUnlock()
|
|
||||||
return nil, errRingShardsDown
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := c.hash.Get(key)
|
|
||||||
if hash == "" {
|
|
||||||
c.mu.RUnlock()
|
|
||||||
return nil, errRingShardsDown
|
|
||||||
}
|
|
||||||
|
|
||||||
shard := c.shards[hash]
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
return shard, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ringShards) GetByName(shardName string) (*ringShard, error) {
|
|
||||||
if shardName == "" {
|
|
||||||
return c.Random()
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mu.RLock()
|
|
||||||
shard := c.shards[shardName]
|
|
||||||
c.mu.RUnlock()
|
|
||||||
return shard, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ringShards) Random() (*ringShard, error) {
|
|
||||||
return c.GetByKey(strconv.Itoa(rand.Int()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// heartbeat monitors state of each shard in the ring.
|
|
||||||
func (c *ringShards) Heartbeat(frequency time.Duration) {
|
|
||||||
ticker := time.NewTicker(frequency)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
for range ticker.C {
|
|
||||||
var rebalance bool
|
|
||||||
|
|
||||||
for _, shard := range c.List() {
|
|
||||||
err := shard.Client.Ping(ctx).Err()
|
|
||||||
isUp := err == nil || err == pool.ErrPoolTimeout
|
|
||||||
if shard.Vote(isUp) {
|
|
||||||
internal.Logger.Printf(context.Background(), "ring shard state changed: %s", shard)
|
|
||||||
rebalance = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if rebalance {
|
|
||||||
c.rebalance()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// rebalance removes dead shards from the Ring.
|
|
||||||
func (c *ringShards) rebalance() {
|
|
||||||
c.mu.RLock()
|
|
||||||
shards := c.shards
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
liveShards := make([]string, 0, len(shards))
|
|
||||||
|
|
||||||
for name, shard := range shards {
|
|
||||||
if shard.IsUp() {
|
|
||||||
liveShards = append(liveShards, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
hash := c.opt.NewConsistentHash(liveShards)
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
c.hash = hash
|
|
||||||
c.numShard = len(liveShards)
|
|
||||||
c.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ringShards) Len() int {
|
|
||||||
c.mu.RLock()
|
|
||||||
l := c.numShard
|
|
||||||
c.mu.RUnlock()
|
|
||||||
return l
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ringShards) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
c.closed = true
|
|
||||||
|
|
||||||
var firstErr error
|
|
||||||
for _, shard := range c.shards {
|
|
||||||
if err := shard.Client.Close(); err != nil && firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.hash = nil
|
|
||||||
c.shards = nil
|
|
||||||
c.list = nil
|
|
||||||
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type ring struct {
|
|
||||||
opt *RingOptions
|
|
||||||
shards *ringShards
|
|
||||||
cmdsInfoCache *cmdsInfoCache //nolint:structcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ring is a Redis client that uses consistent hashing to distribute
|
|
||||||
// keys across multiple Redis servers (shards). It's safe for
|
|
||||||
// concurrent use by multiple goroutines.
|
|
||||||
//
|
|
||||||
// Ring monitors the state of each shard and removes dead shards from
|
|
||||||
// the ring. When a shard comes online it is added back to the ring. This
|
|
||||||
// gives you maximum availability and partition tolerance, but no
|
|
||||||
// consistency between different shards or even clients. Each client
|
|
||||||
// uses shards that are available to the client and does not do any
|
|
||||||
// coordination when shard state is changed.
|
|
||||||
//
|
|
||||||
// Ring should be used when you need multiple Redis servers for caching
|
|
||||||
// and can tolerate losing data when one of the servers dies.
|
|
||||||
// Otherwise you should use Redis Cluster.
|
|
||||||
type Ring struct {
|
|
||||||
*ring
|
|
||||||
cmdable
|
|
||||||
hooks
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRing(opt *RingOptions) *Ring {
|
|
||||||
opt.init()
|
|
||||||
|
|
||||||
ring := Ring{
|
|
||||||
ring: &ring{
|
|
||||||
opt: opt,
|
|
||||||
shards: newRingShards(opt),
|
|
||||||
},
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
|
||||||
|
|
||||||
ring.cmdsInfoCache = newCmdsInfoCache(ring.cmdsInfo)
|
|
||||||
ring.cmdable = ring.Process
|
|
||||||
|
|
||||||
go ring.shards.Heartbeat(opt.HeartbeatFrequency)
|
|
||||||
|
|
||||||
return &ring
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) Context() context.Context {
|
|
||||||
return c.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) WithContext(ctx context.Context) *Ring {
|
|
||||||
if ctx == nil {
|
|
||||||
panic("nil context")
|
|
||||||
}
|
|
||||||
clone := *c
|
|
||||||
clone.cmdable = clone.Process
|
|
||||||
clone.hooks.lock()
|
|
||||||
clone.ctx = ctx
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do creates a Cmd from the args and processes the cmd.
|
|
||||||
func (c *Ring) Do(ctx context.Context, args ...interface{}) *Cmd {
|
|
||||||
cmd := NewCmd(ctx, args...)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) Process(ctx context.Context, cmd Cmder) error {
|
|
||||||
return c.hooks.process(ctx, cmd, c.process)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Options returns read-only Options that were used to create the client.
|
|
||||||
func (c *Ring) Options() *RingOptions {
|
|
||||||
return c.opt
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) retryBackoff(attempt int) time.Duration {
|
|
||||||
return internal.RetryBackoff(attempt, c.opt.MinRetryBackoff, c.opt.MaxRetryBackoff)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PoolStats returns accumulated connection pool stats.
|
|
||||||
func (c *Ring) PoolStats() *PoolStats {
|
|
||||||
shards := c.shards.List()
|
|
||||||
var acc PoolStats
|
|
||||||
for _, shard := range shards {
|
|
||||||
s := shard.Client.connPool.Stats()
|
|
||||||
acc.Hits += s.Hits
|
|
||||||
acc.Misses += s.Misses
|
|
||||||
acc.Timeouts += s.Timeouts
|
|
||||||
acc.TotalConns += s.TotalConns
|
|
||||||
acc.IdleConns += s.IdleConns
|
|
||||||
}
|
|
||||||
return &acc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Len returns the current number of shards in the ring.
|
|
||||||
func (c *Ring) Len() int {
|
|
||||||
return c.shards.Len()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe subscribes the client to the specified channels.
|
|
||||||
func (c *Ring) Subscribe(ctx context.Context, channels ...string) *PubSub {
|
|
||||||
if len(channels) == 0 {
|
|
||||||
panic("at least one channel is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
shard, err := c.shards.GetByKey(channels[0])
|
|
||||||
if err != nil {
|
|
||||||
// TODO: return PubSub with sticky error
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return shard.Client.Subscribe(ctx, channels...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSubscribe subscribes the client to the given patterns.
|
|
||||||
func (c *Ring) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
|
||||||
if len(channels) == 0 {
|
|
||||||
panic("at least one channel is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
shard, err := c.shards.GetByKey(channels[0])
|
|
||||||
if err != nil {
|
|
||||||
// TODO: return PubSub with sticky error
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return shard.Client.PSubscribe(ctx, channels...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ForEachShard concurrently calls the fn on each live shard in the ring.
|
|
||||||
// It returns the first error if any.
|
|
||||||
func (c *Ring) ForEachShard(
|
|
||||||
ctx context.Context,
|
|
||||||
fn func(ctx context.Context, client *Client) error,
|
|
||||||
) error {
|
|
||||||
shards := c.shards.List()
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
errCh := make(chan error, 1)
|
|
||||||
for _, shard := range shards {
|
|
||||||
if shard.IsDown() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Add(1)
|
|
||||||
go func(shard *ringShard) {
|
|
||||||
defer wg.Done()
|
|
||||||
err := fn(ctx, shard.Client)
|
|
||||||
if err != nil {
|
|
||||||
select {
|
|
||||||
case errCh <- err:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}(shard)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case err := <-errCh:
|
|
||||||
return err
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) cmdsInfo(ctx context.Context) (map[string]*CommandInfo, error) {
|
|
||||||
shards := c.shards.List()
|
|
||||||
var firstErr error
|
|
||||||
for _, shard := range shards {
|
|
||||||
cmdsInfo, err := shard.Client.Command(ctx).Result()
|
|
||||||
if err == nil {
|
|
||||||
return cmdsInfo, nil
|
|
||||||
}
|
|
||||||
if firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if firstErr == nil {
|
|
||||||
return nil, errRingShardsDown
|
|
||||||
}
|
|
||||||
return nil, firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) cmdInfo(ctx context.Context, name string) *CommandInfo {
|
|
||||||
cmdsInfo, err := c.cmdsInfoCache.Get(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
info := cmdsInfo[name]
|
|
||||||
if info == nil {
|
|
||||||
internal.Logger.Printf(ctx, "info for cmd=%s not found", name)
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) cmdShard(ctx context.Context, cmd Cmder) (*ringShard, error) {
|
|
||||||
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
|
||||||
pos := cmdFirstKeyPos(cmd, cmdInfo)
|
|
||||||
if pos == 0 {
|
|
||||||
return c.shards.Random()
|
|
||||||
}
|
|
||||||
firstKey := cmd.stringArg(pos)
|
|
||||||
return c.shards.GetByKey(firstKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) process(ctx context.Context, cmd Cmder) error {
|
|
||||||
var lastErr error
|
|
||||||
for attempt := 0; attempt <= c.opt.MaxRetries; attempt++ {
|
|
||||||
if attempt > 0 {
|
|
||||||
if err := internal.Sleep(ctx, c.retryBackoff(attempt)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shard, err := c.cmdShard(ctx, cmd)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
lastErr = shard.Client.Process(ctx, cmd)
|
|
||||||
if lastErr == nil || !shouldRetry(lastErr, cmd.readTimeout() == nil) {
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lastErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.Pipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) Pipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: c.processPipeline,
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) processPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.generalProcessPipeline(ctx, cmds, false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.TxPipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) TxPipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: c.processTxPipeline,
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) processTxPipeline(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processPipeline(ctx, cmds, func(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.generalProcessPipeline(ctx, cmds, true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) generalProcessPipeline(
|
|
||||||
ctx context.Context, cmds []Cmder, tx bool,
|
|
||||||
) error {
|
|
||||||
cmdsMap := make(map[string][]Cmder)
|
|
||||||
for _, cmd := range cmds {
|
|
||||||
cmdInfo := c.cmdInfo(ctx, cmd.Name())
|
|
||||||
hash := cmd.stringArg(cmdFirstKeyPos(cmd, cmdInfo))
|
|
||||||
if hash != "" {
|
|
||||||
hash = c.shards.Hash(hash)
|
|
||||||
}
|
|
||||||
cmdsMap[hash] = append(cmdsMap[hash], cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for hash, cmds := range cmdsMap {
|
|
||||||
wg.Add(1)
|
|
||||||
go func(hash string, cmds []Cmder) {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
_ = c.processShardPipeline(ctx, hash, cmds, tx)
|
|
||||||
}(hash, cmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
return cmdsFirstErr(cmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) processShardPipeline(
|
|
||||||
ctx context.Context, hash string, cmds []Cmder, tx bool,
|
|
||||||
) error {
|
|
||||||
// TODO: retry?
|
|
||||||
shard, err := c.shards.GetByName(hash)
|
|
||||||
if err != nil {
|
|
||||||
setCmdsErr(cmds, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tx {
|
|
||||||
return shard.Client.processTxPipeline(ctx, cmds)
|
|
||||||
}
|
|
||||||
return shard.Client.processPipeline(ctx, cmds)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Ring) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
|
|
||||||
if len(keys) == 0 {
|
|
||||||
return fmt.Errorf("redis: Watch requires at least one key")
|
|
||||||
}
|
|
||||||
|
|
||||||
var shards []*ringShard
|
|
||||||
for _, key := range keys {
|
|
||||||
if key != "" {
|
|
||||||
shard, err := c.shards.GetByKey(hashtag.Key(key))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
shards = append(shards, shard)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(shards) == 0 {
|
|
||||||
return fmt.Errorf("redis: Watch requires at least one shard")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(shards) > 1 {
|
|
||||||
for _, shard := range shards[1:] {
|
|
||||||
if shard.Client != shards[0].Client {
|
|
||||||
err := fmt.Errorf("redis: Watch requires all keys to be in the same shard")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return shards[0].Client.Watch(ctx, fn, keys...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the ring client, releasing any open resources.
|
|
||||||
//
|
|
||||||
// It is rare to Close a Ring, as the Ring is meant to be long-lived
|
|
||||||
// and shared between many goroutines.
|
|
||||||
func (c *Ring) Close() error {
|
|
||||||
return c.shards.Close()
|
|
||||||
}
|
|
@ -1,65 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/hex"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Scripter interface {
|
|
||||||
Eval(ctx context.Context, script string, keys []string, args ...interface{}) *Cmd
|
|
||||||
EvalSha(ctx context.Context, sha1 string, keys []string, args ...interface{}) *Cmd
|
|
||||||
ScriptExists(ctx context.Context, hashes ...string) *BoolSliceCmd
|
|
||||||
ScriptLoad(ctx context.Context, script string) *StringCmd
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ Scripter = (*Client)(nil)
|
|
||||||
_ Scripter = (*Ring)(nil)
|
|
||||||
_ Scripter = (*ClusterClient)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Script struct {
|
|
||||||
src, hash string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewScript(src string) *Script {
|
|
||||||
h := sha1.New()
|
|
||||||
_, _ = io.WriteString(h, src)
|
|
||||||
return &Script{
|
|
||||||
src: src,
|
|
||||||
hash: hex.EncodeToString(h.Sum(nil)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) Hash() string {
|
|
||||||
return s.hash
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) Load(ctx context.Context, c Scripter) *StringCmd {
|
|
||||||
return c.ScriptLoad(ctx, s.src)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) Exists(ctx context.Context, c Scripter) *BoolSliceCmd {
|
|
||||||
return c.ScriptExists(ctx, s.hash)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) Eval(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
|
||||||
return c.Eval(ctx, s.src, keys, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Script) EvalSha(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
|
||||||
return c.EvalSha(ctx, s.hash, keys, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run optimistically uses EVALSHA to run the script. If script does not exist
|
|
||||||
// it is retried using EVAL.
|
|
||||||
func (s *Script) Run(ctx context.Context, c Scripter, keys []string, args ...interface{}) *Cmd {
|
|
||||||
r := s.EvalSha(ctx, c, keys, args...)
|
|
||||||
if err := r.Err(); err != nil && strings.HasPrefix(err.Error(), "NOSCRIPT ") {
|
|
||||||
return s.Eval(ctx, c, keys, args...)
|
|
||||||
}
|
|
||||||
return r
|
|
||||||
}
|
|
@ -1,796 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal"
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
"github.com/go-redis/redis/v8/internal/rand"
|
|
||||||
)
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// FailoverOptions are used to configure a failover client and should
|
|
||||||
// be passed to NewFailoverClient.
|
|
||||||
type FailoverOptions struct {
|
|
||||||
// The master name.
|
|
||||||
MasterName string
|
|
||||||
// A seed list of host:port addresses of sentinel nodes.
|
|
||||||
SentinelAddrs []string
|
|
||||||
|
|
||||||
// If specified with SentinelPassword, enables ACL-based authentication (via
|
|
||||||
// AUTH <user> <pass>).
|
|
||||||
SentinelUsername string
|
|
||||||
// Sentinel password from "requirepass <password>" (if enabled) in Sentinel
|
|
||||||
// configuration, or, if SentinelUsername is also supplied, used for ACL-based
|
|
||||||
// authentication.
|
|
||||||
SentinelPassword string
|
|
||||||
|
|
||||||
// Allows routing read-only commands to the closest master or slave node.
|
|
||||||
// This option only works with NewFailoverClusterClient.
|
|
||||||
RouteByLatency bool
|
|
||||||
// Allows routing read-only commands to the random master or slave node.
|
|
||||||
// This option only works with NewFailoverClusterClient.
|
|
||||||
RouteRandomly bool
|
|
||||||
|
|
||||||
// Route all commands to slave read-only nodes.
|
|
||||||
SlaveOnly bool
|
|
||||||
|
|
||||||
// Use slaves disconnected with master when cannot get connected slaves
|
|
||||||
// Now, this option only works in RandomSlaveAddr function.
|
|
||||||
UseDisconnectedSlaves bool
|
|
||||||
|
|
||||||
// Following options are copied from Options struct.
|
|
||||||
|
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
|
||||||
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
DB int
|
|
||||||
|
|
||||||
MaxRetries int
|
|
||||||
MinRetryBackoff time.Duration
|
|
||||||
MaxRetryBackoff time.Duration
|
|
||||||
|
|
||||||
DialTimeout time.Duration
|
|
||||||
ReadTimeout time.Duration
|
|
||||||
WriteTimeout time.Duration
|
|
||||||
|
|
||||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
|
||||||
PoolFIFO bool
|
|
||||||
|
|
||||||
PoolSize int
|
|
||||||
MinIdleConns int
|
|
||||||
MaxConnAge time.Duration
|
|
||||||
PoolTimeout time.Duration
|
|
||||||
IdleTimeout time.Duration
|
|
||||||
IdleCheckFrequency time.Duration
|
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt *FailoverOptions) clientOptions() *Options {
|
|
||||||
return &Options{
|
|
||||||
Addr: "FailoverClient",
|
|
||||||
|
|
||||||
Dialer: opt.Dialer,
|
|
||||||
OnConnect: opt.OnConnect,
|
|
||||||
|
|
||||||
DB: opt.DB,
|
|
||||||
Username: opt.Username,
|
|
||||||
Password: opt.Password,
|
|
||||||
|
|
||||||
MaxRetries: opt.MaxRetries,
|
|
||||||
MinRetryBackoff: opt.MinRetryBackoff,
|
|
||||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
|
||||||
|
|
||||||
DialTimeout: opt.DialTimeout,
|
|
||||||
ReadTimeout: opt.ReadTimeout,
|
|
||||||
WriteTimeout: opt.WriteTimeout,
|
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
|
||||||
PoolSize: opt.PoolSize,
|
|
||||||
PoolTimeout: opt.PoolTimeout,
|
|
||||||
IdleTimeout: opt.IdleTimeout,
|
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
|
||||||
MinIdleConns: opt.MinIdleConns,
|
|
||||||
MaxConnAge: opt.MaxConnAge,
|
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt *FailoverOptions) sentinelOptions(addr string) *Options {
|
|
||||||
return &Options{
|
|
||||||
Addr: addr,
|
|
||||||
|
|
||||||
Dialer: opt.Dialer,
|
|
||||||
OnConnect: opt.OnConnect,
|
|
||||||
|
|
||||||
DB: 0,
|
|
||||||
Username: opt.SentinelUsername,
|
|
||||||
Password: opt.SentinelPassword,
|
|
||||||
|
|
||||||
MaxRetries: opt.MaxRetries,
|
|
||||||
MinRetryBackoff: opt.MinRetryBackoff,
|
|
||||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
|
||||||
|
|
||||||
DialTimeout: opt.DialTimeout,
|
|
||||||
ReadTimeout: opt.ReadTimeout,
|
|
||||||
WriteTimeout: opt.WriteTimeout,
|
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
|
||||||
PoolSize: opt.PoolSize,
|
|
||||||
PoolTimeout: opt.PoolTimeout,
|
|
||||||
IdleTimeout: opt.IdleTimeout,
|
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
|
||||||
MinIdleConns: opt.MinIdleConns,
|
|
||||||
MaxConnAge: opt.MaxConnAge,
|
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (opt *FailoverOptions) clusterOptions() *ClusterOptions {
|
|
||||||
return &ClusterOptions{
|
|
||||||
Dialer: opt.Dialer,
|
|
||||||
OnConnect: opt.OnConnect,
|
|
||||||
|
|
||||||
Username: opt.Username,
|
|
||||||
Password: opt.Password,
|
|
||||||
|
|
||||||
MaxRedirects: opt.MaxRetries,
|
|
||||||
|
|
||||||
RouteByLatency: opt.RouteByLatency,
|
|
||||||
RouteRandomly: opt.RouteRandomly,
|
|
||||||
|
|
||||||
MinRetryBackoff: opt.MinRetryBackoff,
|
|
||||||
MaxRetryBackoff: opt.MaxRetryBackoff,
|
|
||||||
|
|
||||||
DialTimeout: opt.DialTimeout,
|
|
||||||
ReadTimeout: opt.ReadTimeout,
|
|
||||||
WriteTimeout: opt.WriteTimeout,
|
|
||||||
|
|
||||||
PoolFIFO: opt.PoolFIFO,
|
|
||||||
PoolSize: opt.PoolSize,
|
|
||||||
PoolTimeout: opt.PoolTimeout,
|
|
||||||
IdleTimeout: opt.IdleTimeout,
|
|
||||||
IdleCheckFrequency: opt.IdleCheckFrequency,
|
|
||||||
MinIdleConns: opt.MinIdleConns,
|
|
||||||
MaxConnAge: opt.MaxConnAge,
|
|
||||||
|
|
||||||
TLSConfig: opt.TLSConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewFailoverClient returns a Redis client that uses Redis Sentinel
|
|
||||||
// for automatic failover. It's safe for concurrent use by multiple
|
|
||||||
// goroutines.
|
|
||||||
func NewFailoverClient(failoverOpt *FailoverOptions) *Client {
|
|
||||||
if failoverOpt.RouteByLatency {
|
|
||||||
panic("to route commands by latency, use NewFailoverClusterClient")
|
|
||||||
}
|
|
||||||
if failoverOpt.RouteRandomly {
|
|
||||||
panic("to route commands randomly, use NewFailoverClusterClient")
|
|
||||||
}
|
|
||||||
|
|
||||||
sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
|
|
||||||
copy(sentinelAddrs, failoverOpt.SentinelAddrs)
|
|
||||||
|
|
||||||
rand.Shuffle(len(sentinelAddrs), func(i, j int) {
|
|
||||||
sentinelAddrs[i], sentinelAddrs[j] = sentinelAddrs[j], sentinelAddrs[i]
|
|
||||||
})
|
|
||||||
|
|
||||||
failover := &sentinelFailover{
|
|
||||||
opt: failoverOpt,
|
|
||||||
sentinelAddrs: sentinelAddrs,
|
|
||||||
}
|
|
||||||
|
|
||||||
opt := failoverOpt.clientOptions()
|
|
||||||
opt.Dialer = masterSlaveDialer(failover)
|
|
||||||
opt.init()
|
|
||||||
|
|
||||||
connPool := newConnPool(opt)
|
|
||||||
|
|
||||||
failover.mu.Lock()
|
|
||||||
failover.onFailover = func(ctx context.Context, addr string) {
|
|
||||||
_ = connPool.Filter(func(cn *pool.Conn) bool {
|
|
||||||
return cn.RemoteAddr().String() != addr
|
|
||||||
})
|
|
||||||
}
|
|
||||||
failover.mu.Unlock()
|
|
||||||
|
|
||||||
c := Client{
|
|
||||||
baseClient: newBaseClient(opt, connPool),
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
|
||||||
c.cmdable = c.Process
|
|
||||||
c.onClose = failover.Close
|
|
||||||
|
|
||||||
return &c
|
|
||||||
}
|
|
||||||
|
|
||||||
func masterSlaveDialer(
|
|
||||||
failover *sentinelFailover,
|
|
||||||
) func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return func(ctx context.Context, network, _ string) (net.Conn, error) {
|
|
||||||
var addr string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if failover.opt.SlaveOnly {
|
|
||||||
addr, err = failover.RandomSlaveAddr(ctx)
|
|
||||||
} else {
|
|
||||||
addr, err = failover.MasterAddr(ctx)
|
|
||||||
if err == nil {
|
|
||||||
failover.trySwitchMaster(ctx, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if failover.opt.Dialer != nil {
|
|
||||||
return failover.opt.Dialer(ctx, network, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
netDialer := &net.Dialer{
|
|
||||||
Timeout: failover.opt.DialTimeout,
|
|
||||||
KeepAlive: 5 * time.Minute,
|
|
||||||
}
|
|
||||||
if failover.opt.TLSConfig == nil {
|
|
||||||
return netDialer.DialContext(ctx, network, addr)
|
|
||||||
}
|
|
||||||
return tls.DialWithDialer(netDialer, network, addr, failover.opt.TLSConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// SentinelClient is a client for a Redis Sentinel.
|
|
||||||
type SentinelClient struct {
|
|
||||||
*baseClient
|
|
||||||
hooks
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSentinelClient(opt *Options) *SentinelClient {
|
|
||||||
opt.init()
|
|
||||||
c := &SentinelClient{
|
|
||||||
baseClient: &baseClient{
|
|
||||||
opt: opt,
|
|
||||||
connPool: newConnPool(opt),
|
|
||||||
},
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SentinelClient) Context() context.Context {
|
|
||||||
return c.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SentinelClient) WithContext(ctx context.Context) *SentinelClient {
|
|
||||||
if ctx == nil {
|
|
||||||
panic("nil context")
|
|
||||||
}
|
|
||||||
clone := *c
|
|
||||||
clone.ctx = ctx
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SentinelClient) Process(ctx context.Context, cmd Cmder) error {
|
|
||||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SentinelClient) pubSub() *PubSub {
|
|
||||||
pubsub := &PubSub{
|
|
||||||
opt: c.opt,
|
|
||||||
|
|
||||||
newConn: func(ctx context.Context, channels []string) (*pool.Conn, error) {
|
|
||||||
return c.newConn(ctx)
|
|
||||||
},
|
|
||||||
closeConn: c.connPool.CloseConn,
|
|
||||||
}
|
|
||||||
pubsub.init()
|
|
||||||
return pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping is used to test if a connection is still alive, or to
|
|
||||||
// measure latency.
|
|
||||||
func (c *SentinelClient) Ping(ctx context.Context) *StringCmd {
|
|
||||||
cmd := NewStringCmd(ctx, "ping")
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subscribe subscribes the client to the specified channels.
|
|
||||||
// Channels can be omitted to create empty subscription.
|
|
||||||
func (c *SentinelClient) Subscribe(ctx context.Context, channels ...string) *PubSub {
|
|
||||||
pubsub := c.pubSub()
|
|
||||||
if len(channels) > 0 {
|
|
||||||
_ = pubsub.Subscribe(ctx, channels...)
|
|
||||||
}
|
|
||||||
return pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
// PSubscribe subscribes the client to the given patterns.
|
|
||||||
// Patterns can be omitted to create empty subscription.
|
|
||||||
func (c *SentinelClient) PSubscribe(ctx context.Context, channels ...string) *PubSub {
|
|
||||||
pubsub := c.pubSub()
|
|
||||||
if len(channels) > 0 {
|
|
||||||
_ = pubsub.PSubscribe(ctx, channels...)
|
|
||||||
}
|
|
||||||
return pubsub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SentinelClient) GetMasterAddrByName(ctx context.Context, name string) *StringSliceCmd {
|
|
||||||
cmd := NewStringSliceCmd(ctx, "sentinel", "get-master-addr-by-name", name)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *SentinelClient) Sentinels(ctx context.Context, name string) *SliceCmd {
|
|
||||||
cmd := NewSliceCmd(ctx, "sentinel", "sentinels", name)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failover forces a failover as if the master was not reachable, and without
|
|
||||||
// asking for agreement to other Sentinels.
|
|
||||||
func (c *SentinelClient) Failover(ctx context.Context, name string) *StatusCmd {
|
|
||||||
cmd := NewStatusCmd(ctx, "sentinel", "failover", name)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset resets all the masters with matching name. The pattern argument is a
|
|
||||||
// glob-style pattern. The reset process clears any previous state in a master
|
|
||||||
// (including a failover in progress), and removes every slave and sentinel
|
|
||||||
// already discovered and associated with the master.
|
|
||||||
func (c *SentinelClient) Reset(ctx context.Context, pattern string) *IntCmd {
|
|
||||||
cmd := NewIntCmd(ctx, "sentinel", "reset", pattern)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// FlushConfig forces Sentinel to rewrite its configuration on disk, including
|
|
||||||
// the current Sentinel state.
|
|
||||||
func (c *SentinelClient) FlushConfig(ctx context.Context) *StatusCmd {
|
|
||||||
cmd := NewStatusCmd(ctx, "sentinel", "flushconfig")
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Master shows the state and info of the specified master.
|
|
||||||
func (c *SentinelClient) Master(ctx context.Context, name string) *StringStringMapCmd {
|
|
||||||
cmd := NewStringStringMapCmd(ctx, "sentinel", "master", name)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Masters shows a list of monitored masters and their state.
|
|
||||||
func (c *SentinelClient) Masters(ctx context.Context) *SliceCmd {
|
|
||||||
cmd := NewSliceCmd(ctx, "sentinel", "masters")
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Slaves shows a list of slaves for the specified master and their state.
|
|
||||||
func (c *SentinelClient) Slaves(ctx context.Context, name string) *SliceCmd {
|
|
||||||
cmd := NewSliceCmd(ctx, "sentinel", "slaves", name)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// CkQuorum checks if the current Sentinel configuration is able to reach the
|
|
||||||
// quorum needed to failover a master, and the majority needed to authorize the
|
|
||||||
// failover. This command should be used in monitoring systems to check if a
|
|
||||||
// Sentinel deployment is ok.
|
|
||||||
func (c *SentinelClient) CkQuorum(ctx context.Context, name string) *StringCmd {
|
|
||||||
cmd := NewStringCmd(ctx, "sentinel", "ckquorum", name)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Monitor tells the Sentinel to start monitoring a new master with the specified
|
|
||||||
// name, ip, port, and quorum.
|
|
||||||
func (c *SentinelClient) Monitor(ctx context.Context, name, ip, port, quorum string) *StringCmd {
|
|
||||||
cmd := NewStringCmd(ctx, "sentinel", "monitor", name, ip, port, quorum)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set is used in order to change configuration parameters of a specific master.
|
|
||||||
func (c *SentinelClient) Set(ctx context.Context, name, option, value string) *StringCmd {
|
|
||||||
cmd := NewStringCmd(ctx, "sentinel", "set", name, option, value)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove is used in order to remove the specified master: the master will no
|
|
||||||
// longer be monitored, and will totally be removed from the internal state of
|
|
||||||
// the Sentinel.
|
|
||||||
func (c *SentinelClient) Remove(ctx context.Context, name string) *StringCmd {
|
|
||||||
cmd := NewStringCmd(ctx, "sentinel", "remove", name)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
type sentinelFailover struct {
|
|
||||||
opt *FailoverOptions
|
|
||||||
|
|
||||||
sentinelAddrs []string
|
|
||||||
|
|
||||||
onFailover func(ctx context.Context, addr string)
|
|
||||||
onUpdate func(ctx context.Context)
|
|
||||||
|
|
||||||
mu sync.RWMutex
|
|
||||||
_masterAddr string
|
|
||||||
sentinel *SentinelClient
|
|
||||||
pubsub *PubSub
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) Close() error {
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
if c.sentinel != nil {
|
|
||||||
return c.closeSentinel()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) closeSentinel() error {
|
|
||||||
firstErr := c.pubsub.Close()
|
|
||||||
c.pubsub = nil
|
|
||||||
|
|
||||||
err := c.sentinel.Close()
|
|
||||||
if err != nil && firstErr == nil {
|
|
||||||
firstErr = err
|
|
||||||
}
|
|
||||||
c.sentinel = nil
|
|
||||||
|
|
||||||
return firstErr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) RandomSlaveAddr(ctx context.Context) (string, error) {
|
|
||||||
if c.opt == nil {
|
|
||||||
return "", errors.New("opt is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses, err := c.slaveAddrs(ctx, false)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(addresses) == 0 && c.opt.UseDisconnectedSlaves {
|
|
||||||
addresses, err = c.slaveAddrs(ctx, true)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(addresses) == 0 {
|
|
||||||
return c.MasterAddr(ctx)
|
|
||||||
}
|
|
||||||
return addresses[rand.Intn(len(addresses))], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) MasterAddr(ctx context.Context) (string, error) {
|
|
||||||
c.mu.RLock()
|
|
||||||
sentinel := c.sentinel
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if sentinel != nil {
|
|
||||||
addr := c.getMasterAddr(ctx, sentinel)
|
|
||||||
if addr != "" {
|
|
||||||
return addr, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.sentinel != nil {
|
|
||||||
addr := c.getMasterAddr(ctx, c.sentinel)
|
|
||||||
if addr != "" {
|
|
||||||
return addr, nil
|
|
||||||
}
|
|
||||||
_ = c.closeSentinel()
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, sentinelAddr := range c.sentinelAddrs {
|
|
||||||
sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))
|
|
||||||
|
|
||||||
masterAddr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
|
|
||||||
if err != nil {
|
|
||||||
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName master=%q failed: %s",
|
|
||||||
c.opt.MasterName, err)
|
|
||||||
_ = sentinel.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push working sentinel to the top.
|
|
||||||
c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]
|
|
||||||
c.setSentinel(ctx, sentinel)
|
|
||||||
|
|
||||||
addr := net.JoinHostPort(masterAddr[0], masterAddr[1])
|
|
||||||
return addr, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.New("redis: all sentinels specified in configuration are unreachable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) slaveAddrs(ctx context.Context, useDisconnected bool) ([]string, error) {
|
|
||||||
c.mu.RLock()
|
|
||||||
sentinel := c.sentinel
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if sentinel != nil {
|
|
||||||
addrs := c.getSlaveAddrs(ctx, sentinel)
|
|
||||||
if len(addrs) > 0 {
|
|
||||||
return addrs, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.sentinel != nil {
|
|
||||||
addrs := c.getSlaveAddrs(ctx, c.sentinel)
|
|
||||||
if len(addrs) > 0 {
|
|
||||||
return addrs, nil
|
|
||||||
}
|
|
||||||
_ = c.closeSentinel()
|
|
||||||
}
|
|
||||||
|
|
||||||
var sentinelReachable bool
|
|
||||||
|
|
||||||
for i, sentinelAddr := range c.sentinelAddrs {
|
|
||||||
sentinel := NewSentinelClient(c.opt.sentinelOptions(sentinelAddr))
|
|
||||||
|
|
||||||
slaves, err := sentinel.Slaves(ctx, c.opt.MasterName).Result()
|
|
||||||
if err != nil {
|
|
||||||
internal.Logger.Printf(ctx, "sentinel: Slaves master=%q failed: %s",
|
|
||||||
c.opt.MasterName, err)
|
|
||||||
_ = sentinel.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sentinelReachable = true
|
|
||||||
addrs := parseSlaveAddrs(slaves, useDisconnected)
|
|
||||||
if len(addrs) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Push working sentinel to the top.
|
|
||||||
c.sentinelAddrs[0], c.sentinelAddrs[i] = c.sentinelAddrs[i], c.sentinelAddrs[0]
|
|
||||||
c.setSentinel(ctx, sentinel)
|
|
||||||
|
|
||||||
return addrs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if sentinelReachable {
|
|
||||||
return []string{}, nil
|
|
||||||
}
|
|
||||||
return []string{}, errors.New("redis: all sentinels specified in configuration are unreachable")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) getMasterAddr(ctx context.Context, sentinel *SentinelClient) string {
|
|
||||||
addr, err := sentinel.GetMasterAddrByName(ctx, c.opt.MasterName).Result()
|
|
||||||
if err != nil {
|
|
||||||
internal.Logger.Printf(ctx, "sentinel: GetMasterAddrByName name=%q failed: %s",
|
|
||||||
c.opt.MasterName, err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return net.JoinHostPort(addr[0], addr[1])
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) getSlaveAddrs(ctx context.Context, sentinel *SentinelClient) []string {
|
|
||||||
addrs, err := sentinel.Slaves(ctx, c.opt.MasterName).Result()
|
|
||||||
if err != nil {
|
|
||||||
internal.Logger.Printf(ctx, "sentinel: Slaves name=%q failed: %s",
|
|
||||||
c.opt.MasterName, err)
|
|
||||||
return []string{}
|
|
||||||
}
|
|
||||||
return parseSlaveAddrs(addrs, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSlaveAddrs(addrs []interface{}, keepDisconnected bool) []string {
|
|
||||||
nodes := make([]string, 0, len(addrs))
|
|
||||||
for _, node := range addrs {
|
|
||||||
ip := ""
|
|
||||||
port := ""
|
|
||||||
flags := []string{}
|
|
||||||
lastkey := ""
|
|
||||||
isDown := false
|
|
||||||
|
|
||||||
for _, key := range node.([]interface{}) {
|
|
||||||
switch lastkey {
|
|
||||||
case "ip":
|
|
||||||
ip = key.(string)
|
|
||||||
case "port":
|
|
||||||
port = key.(string)
|
|
||||||
case "flags":
|
|
||||||
flags = strings.Split(key.(string), ",")
|
|
||||||
}
|
|
||||||
lastkey = key.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, flag := range flags {
|
|
||||||
switch flag {
|
|
||||||
case "s_down", "o_down":
|
|
||||||
isDown = true
|
|
||||||
case "disconnected":
|
|
||||||
if !keepDisconnected {
|
|
||||||
isDown = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isDown {
|
|
||||||
nodes = append(nodes, net.JoinHostPort(ip, port))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) trySwitchMaster(ctx context.Context, addr string) {
|
|
||||||
c.mu.RLock()
|
|
||||||
currentAddr := c._masterAddr //nolint:ifshort
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if addr == currentAddr {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.mu.Lock()
|
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if addr == c._masterAddr {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c._masterAddr = addr
|
|
||||||
|
|
||||||
internal.Logger.Printf(ctx, "sentinel: new master=%q addr=%q",
|
|
||||||
c.opt.MasterName, addr)
|
|
||||||
if c.onFailover != nil {
|
|
||||||
c.onFailover(ctx, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) setSentinel(ctx context.Context, sentinel *SentinelClient) {
|
|
||||||
if c.sentinel != nil {
|
|
||||||
panic("not reached")
|
|
||||||
}
|
|
||||||
c.sentinel = sentinel
|
|
||||||
c.discoverSentinels(ctx)
|
|
||||||
|
|
||||||
c.pubsub = sentinel.Subscribe(ctx, "+switch-master", "+slave-reconf-done")
|
|
||||||
go c.listen(c.pubsub)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) discoverSentinels(ctx context.Context) {
|
|
||||||
sentinels, err := c.sentinel.Sentinels(ctx, c.opt.MasterName).Result()
|
|
||||||
if err != nil {
|
|
||||||
internal.Logger.Printf(ctx, "sentinel: Sentinels master=%q failed: %s", c.opt.MasterName, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, sentinel := range sentinels {
|
|
||||||
vals := sentinel.([]interface{})
|
|
||||||
var ip, port string
|
|
||||||
for i := 0; i < len(vals); i += 2 {
|
|
||||||
key := vals[i].(string)
|
|
||||||
switch key {
|
|
||||||
case "ip":
|
|
||||||
ip = vals[i+1].(string)
|
|
||||||
case "port":
|
|
||||||
port = vals[i+1].(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ip != "" && port != "" {
|
|
||||||
sentinelAddr := net.JoinHostPort(ip, port)
|
|
||||||
if !contains(c.sentinelAddrs, sentinelAddr) {
|
|
||||||
internal.Logger.Printf(ctx, "sentinel: discovered new sentinel=%q for master=%q",
|
|
||||||
sentinelAddr, c.opt.MasterName)
|
|
||||||
c.sentinelAddrs = append(c.sentinelAddrs, sentinelAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *sentinelFailover) listen(pubsub *PubSub) {
|
|
||||||
ctx := context.TODO()
|
|
||||||
|
|
||||||
if c.onUpdate != nil {
|
|
||||||
c.onUpdate(ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
ch := pubsub.Channel()
|
|
||||||
for msg := range ch {
|
|
||||||
if msg.Channel == "+switch-master" {
|
|
||||||
parts := strings.Split(msg.Payload, " ")
|
|
||||||
if parts[0] != c.opt.MasterName {
|
|
||||||
internal.Logger.Printf(pubsub.getContext(), "sentinel: ignore addr for master=%q", parts[0])
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
addr := net.JoinHostPort(parts[3], parts[4])
|
|
||||||
c.trySwitchMaster(pubsub.getContext(), addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.onUpdate != nil {
|
|
||||||
c.onUpdate(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(slice []string, str string) bool {
|
|
||||||
for _, s := range slice {
|
|
||||||
if s == str {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// NewFailoverClusterClient returns a client that supports routing read-only commands
|
|
||||||
// to a slave node.
|
|
||||||
func NewFailoverClusterClient(failoverOpt *FailoverOptions) *ClusterClient {
|
|
||||||
sentinelAddrs := make([]string, len(failoverOpt.SentinelAddrs))
|
|
||||||
copy(sentinelAddrs, failoverOpt.SentinelAddrs)
|
|
||||||
|
|
||||||
failover := &sentinelFailover{
|
|
||||||
opt: failoverOpt,
|
|
||||||
sentinelAddrs: sentinelAddrs,
|
|
||||||
}
|
|
||||||
|
|
||||||
opt := failoverOpt.clusterOptions()
|
|
||||||
opt.ClusterSlots = func(ctx context.Context) ([]ClusterSlot, error) {
|
|
||||||
masterAddr, err := failover.MasterAddr(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := []ClusterNode{{
|
|
||||||
Addr: masterAddr,
|
|
||||||
}}
|
|
||||||
|
|
||||||
slaveAddrs, err := failover.slaveAddrs(ctx, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, slaveAddr := range slaveAddrs {
|
|
||||||
nodes = append(nodes, ClusterNode{
|
|
||||||
Addr: slaveAddr,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
slots := []ClusterSlot{
|
|
||||||
{
|
|
||||||
Start: 0,
|
|
||||||
End: 16383,
|
|
||||||
Nodes: nodes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return slots, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
c := NewClusterClient(opt)
|
|
||||||
|
|
||||||
failover.mu.Lock()
|
|
||||||
failover.onUpdate = func(ctx context.Context) {
|
|
||||||
c.ReloadState(ctx)
|
|
||||||
}
|
|
||||||
failover.mu.Unlock()
|
|
||||||
|
|
||||||
return c
|
|
||||||
}
|
|
@ -1,149 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8/internal/pool"
|
|
||||||
"github.com/go-redis/redis/v8/internal/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TxFailedErr transaction redis failed.
|
|
||||||
const TxFailedErr = proto.RedisError("redis: transaction failed")
|
|
||||||
|
|
||||||
// Tx implements Redis transactions as described in
|
|
||||||
// http://redis.io/topics/transactions. It's NOT safe for concurrent use
|
|
||||||
// by multiple goroutines, because Exec resets list of watched keys.
|
|
||||||
//
|
|
||||||
// If you don't need WATCH, use Pipeline instead.
|
|
||||||
type Tx struct {
|
|
||||||
baseClient
|
|
||||||
cmdable
|
|
||||||
statefulCmdable
|
|
||||||
hooks
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) newTx(ctx context.Context) *Tx {
|
|
||||||
tx := Tx{
|
|
||||||
baseClient: baseClient{
|
|
||||||
opt: c.opt,
|
|
||||||
connPool: pool.NewStickyConnPool(c.connPool),
|
|
||||||
},
|
|
||||||
hooks: c.hooks.clone(),
|
|
||||||
ctx: ctx,
|
|
||||||
}
|
|
||||||
tx.init()
|
|
||||||
return &tx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Tx) init() {
|
|
||||||
c.cmdable = c.Process
|
|
||||||
c.statefulCmdable = c.Process
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Tx) Context() context.Context {
|
|
||||||
return c.ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Tx) WithContext(ctx context.Context) *Tx {
|
|
||||||
if ctx == nil {
|
|
||||||
panic("nil context")
|
|
||||||
}
|
|
||||||
clone := *c
|
|
||||||
clone.init()
|
|
||||||
clone.hooks.lock()
|
|
||||||
clone.ctx = ctx
|
|
||||||
return &clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Tx) Process(ctx context.Context, cmd Cmder) error {
|
|
||||||
return c.hooks.process(ctx, cmd, c.baseClient.process)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch prepares a transaction and marks the keys to be watched
|
|
||||||
// for conditional execution if there are any keys.
|
|
||||||
//
|
|
||||||
// The transaction is automatically closed when fn exits.
|
|
||||||
func (c *Client) Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error {
|
|
||||||
tx := c.newTx(ctx)
|
|
||||||
defer tx.Close(ctx)
|
|
||||||
if len(keys) > 0 {
|
|
||||||
if err := tx.Watch(ctx, keys...).Err(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fn(tx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the transaction, releasing any open resources.
|
|
||||||
func (c *Tx) Close(ctx context.Context) error {
|
|
||||||
_ = c.Unwatch(ctx).Err()
|
|
||||||
return c.baseClient.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch marks the keys to be watched for conditional execution
|
|
||||||
// of a transaction.
|
|
||||||
func (c *Tx) Watch(ctx context.Context, keys ...string) *StatusCmd {
|
|
||||||
args := make([]interface{}, 1+len(keys))
|
|
||||||
args[0] = "watch"
|
|
||||||
for i, key := range keys {
|
|
||||||
args[1+i] = key
|
|
||||||
}
|
|
||||||
cmd := NewStatusCmd(ctx, args...)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unwatch flushes all the previously watched keys for a transaction.
|
|
||||||
func (c *Tx) Unwatch(ctx context.Context, keys ...string) *StatusCmd {
|
|
||||||
args := make([]interface{}, 1+len(keys))
|
|
||||||
args[0] = "unwatch"
|
|
||||||
for i, key := range keys {
|
|
||||||
args[1+i] = key
|
|
||||||
}
|
|
||||||
cmd := NewStatusCmd(ctx, args...)
|
|
||||||
_ = c.Process(ctx, cmd)
|
|
||||||
return cmd
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pipeline creates a pipeline. Usually it is more convenient to use Pipelined.
|
|
||||||
func (c *Tx) Pipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processPipeline(ctx, cmds, c.baseClient.processPipeline)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pipelined executes commands queued in the fn outside of the transaction.
|
|
||||||
// Use TxPipelined if you need transactional behavior.
|
|
||||||
func (c *Tx) Pipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.Pipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxPipelined executes commands queued in the fn in the transaction.
|
|
||||||
//
|
|
||||||
// When using WATCH, EXEC will execute commands only if the watched keys
|
|
||||||
// were not modified, allowing for a check-and-set mechanism.
|
|
||||||
//
|
|
||||||
// Exec always returns list of commands. If transaction fails
|
|
||||||
// TxFailedErr is returned. Otherwise Exec returns an error of the first
|
|
||||||
// failed command or nil.
|
|
||||||
func (c *Tx) TxPipelined(ctx context.Context, fn func(Pipeliner) error) ([]Cmder, error) {
|
|
||||||
return c.TxPipeline().Pipelined(ctx, fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TxPipeline creates a pipeline. Usually it is more convenient to use TxPipelined.
|
|
||||||
func (c *Tx) TxPipeline() Pipeliner {
|
|
||||||
pipe := Pipeline{
|
|
||||||
ctx: c.ctx,
|
|
||||||
exec: func(ctx context.Context, cmds []Cmder) error {
|
|
||||||
return c.hooks.processTxPipeline(ctx, cmds, c.baseClient.processTxPipeline)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
pipe.init()
|
|
||||||
return &pipe
|
|
||||||
}
|
|
@ -1,215 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UniversalOptions information is required by UniversalClient to establish
|
|
||||||
// connections.
|
|
||||||
type UniversalOptions struct {
|
|
||||||
// Either a single address or a seed list of host:port addresses
|
|
||||||
// of cluster/sentinel nodes.
|
|
||||||
Addrs []string
|
|
||||||
|
|
||||||
// Database to be selected after connecting to the server.
|
|
||||||
// Only single-node and failover clients.
|
|
||||||
DB int
|
|
||||||
|
|
||||||
// Common options.
|
|
||||||
|
|
||||||
Dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
|
||||||
OnConnect func(ctx context.Context, cn *Conn) error
|
|
||||||
|
|
||||||
Username string
|
|
||||||
Password string
|
|
||||||
SentinelUsername string
|
|
||||||
SentinelPassword string
|
|
||||||
|
|
||||||
MaxRetries int
|
|
||||||
MinRetryBackoff time.Duration
|
|
||||||
MaxRetryBackoff time.Duration
|
|
||||||
|
|
||||||
DialTimeout time.Duration
|
|
||||||
ReadTimeout time.Duration
|
|
||||||
WriteTimeout time.Duration
|
|
||||||
|
|
||||||
// PoolFIFO uses FIFO mode for each node connection pool GET/PUT (default LIFO).
|
|
||||||
PoolFIFO bool
|
|
||||||
|
|
||||||
PoolSize int
|
|
||||||
MinIdleConns int
|
|
||||||
MaxConnAge time.Duration
|
|
||||||
PoolTimeout time.Duration
|
|
||||||
IdleTimeout time.Duration
|
|
||||||
IdleCheckFrequency time.Duration
|
|
||||||
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
|
|
||||||
// Only cluster clients.
|
|
||||||
|
|
||||||
MaxRedirects int
|
|
||||||
ReadOnly bool
|
|
||||||
RouteByLatency bool
|
|
||||||
RouteRandomly bool
|
|
||||||
|
|
||||||
// The sentinel master name.
|
|
||||||
// Only failover clients.
|
|
||||||
|
|
||||||
MasterName string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cluster returns cluster options created from the universal options.
|
|
||||||
func (o *UniversalOptions) Cluster() *ClusterOptions {
|
|
||||||
if len(o.Addrs) == 0 {
|
|
||||||
o.Addrs = []string{"127.0.0.1:6379"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ClusterOptions{
|
|
||||||
Addrs: o.Addrs,
|
|
||||||
Dialer: o.Dialer,
|
|
||||||
OnConnect: o.OnConnect,
|
|
||||||
|
|
||||||
Username: o.Username,
|
|
||||||
Password: o.Password,
|
|
||||||
|
|
||||||
MaxRedirects: o.MaxRedirects,
|
|
||||||
ReadOnly: o.ReadOnly,
|
|
||||||
RouteByLatency: o.RouteByLatency,
|
|
||||||
RouteRandomly: o.RouteRandomly,
|
|
||||||
|
|
||||||
MaxRetries: o.MaxRetries,
|
|
||||||
MinRetryBackoff: o.MinRetryBackoff,
|
|
||||||
MaxRetryBackoff: o.MaxRetryBackoff,
|
|
||||||
|
|
||||||
DialTimeout: o.DialTimeout,
|
|
||||||
ReadTimeout: o.ReadTimeout,
|
|
||||||
WriteTimeout: o.WriteTimeout,
|
|
||||||
PoolFIFO: o.PoolFIFO,
|
|
||||||
PoolSize: o.PoolSize,
|
|
||||||
MinIdleConns: o.MinIdleConns,
|
|
||||||
MaxConnAge: o.MaxConnAge,
|
|
||||||
PoolTimeout: o.PoolTimeout,
|
|
||||||
IdleTimeout: o.IdleTimeout,
|
|
||||||
IdleCheckFrequency: o.IdleCheckFrequency,
|
|
||||||
|
|
||||||
TLSConfig: o.TLSConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Failover returns failover options created from the universal options.
|
|
||||||
func (o *UniversalOptions) Failover() *FailoverOptions {
|
|
||||||
if len(o.Addrs) == 0 {
|
|
||||||
o.Addrs = []string{"127.0.0.1:26379"}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &FailoverOptions{
|
|
||||||
SentinelAddrs: o.Addrs,
|
|
||||||
MasterName: o.MasterName,
|
|
||||||
|
|
||||||
Dialer: o.Dialer,
|
|
||||||
OnConnect: o.OnConnect,
|
|
||||||
|
|
||||||
DB: o.DB,
|
|
||||||
Username: o.Username,
|
|
||||||
Password: o.Password,
|
|
||||||
SentinelUsername: o.SentinelUsername,
|
|
||||||
SentinelPassword: o.SentinelPassword,
|
|
||||||
|
|
||||||
MaxRetries: o.MaxRetries,
|
|
||||||
MinRetryBackoff: o.MinRetryBackoff,
|
|
||||||
MaxRetryBackoff: o.MaxRetryBackoff,
|
|
||||||
|
|
||||||
DialTimeout: o.DialTimeout,
|
|
||||||
ReadTimeout: o.ReadTimeout,
|
|
||||||
WriteTimeout: o.WriteTimeout,
|
|
||||||
|
|
||||||
PoolFIFO: o.PoolFIFO,
|
|
||||||
PoolSize: o.PoolSize,
|
|
||||||
MinIdleConns: o.MinIdleConns,
|
|
||||||
MaxConnAge: o.MaxConnAge,
|
|
||||||
PoolTimeout: o.PoolTimeout,
|
|
||||||
IdleTimeout: o.IdleTimeout,
|
|
||||||
IdleCheckFrequency: o.IdleCheckFrequency,
|
|
||||||
|
|
||||||
TLSConfig: o.TLSConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple returns basic options created from the universal options.
|
|
||||||
func (o *UniversalOptions) Simple() *Options {
|
|
||||||
addr := "127.0.0.1:6379"
|
|
||||||
if len(o.Addrs) > 0 {
|
|
||||||
addr = o.Addrs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Options{
|
|
||||||
Addr: addr,
|
|
||||||
Dialer: o.Dialer,
|
|
||||||
OnConnect: o.OnConnect,
|
|
||||||
|
|
||||||
DB: o.DB,
|
|
||||||
Username: o.Username,
|
|
||||||
Password: o.Password,
|
|
||||||
|
|
||||||
MaxRetries: o.MaxRetries,
|
|
||||||
MinRetryBackoff: o.MinRetryBackoff,
|
|
||||||
MaxRetryBackoff: o.MaxRetryBackoff,
|
|
||||||
|
|
||||||
DialTimeout: o.DialTimeout,
|
|
||||||
ReadTimeout: o.ReadTimeout,
|
|
||||||
WriteTimeout: o.WriteTimeout,
|
|
||||||
|
|
||||||
PoolFIFO: o.PoolFIFO,
|
|
||||||
PoolSize: o.PoolSize,
|
|
||||||
MinIdleConns: o.MinIdleConns,
|
|
||||||
MaxConnAge: o.MaxConnAge,
|
|
||||||
PoolTimeout: o.PoolTimeout,
|
|
||||||
IdleTimeout: o.IdleTimeout,
|
|
||||||
IdleCheckFrequency: o.IdleCheckFrequency,
|
|
||||||
|
|
||||||
TLSConfig: o.TLSConfig,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------------------------
|
|
||||||
|
|
||||||
// UniversalClient is an abstract client which - based on the provided options -
|
|
||||||
// represents either a ClusterClient, a FailoverClient, or a single-node Client.
|
|
||||||
// This can be useful for testing cluster-specific applications locally or having different
|
|
||||||
// clients in different environments.
|
|
||||||
type UniversalClient interface {
|
|
||||||
Cmdable
|
|
||||||
Context() context.Context
|
|
||||||
AddHook(Hook)
|
|
||||||
Watch(ctx context.Context, fn func(*Tx) error, keys ...string) error
|
|
||||||
Do(ctx context.Context, args ...interface{}) *Cmd
|
|
||||||
Process(ctx context.Context, cmd Cmder) error
|
|
||||||
Subscribe(ctx context.Context, channels ...string) *PubSub
|
|
||||||
PSubscribe(ctx context.Context, channels ...string) *PubSub
|
|
||||||
Close() error
|
|
||||||
PoolStats() *PoolStats
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ UniversalClient = (*Client)(nil)
|
|
||||||
_ UniversalClient = (*ClusterClient)(nil)
|
|
||||||
_ UniversalClient = (*Ring)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewUniversalClient returns a new multi client. The type of the returned client depends
|
|
||||||
// on the following conditions:
|
|
||||||
//
|
|
||||||
// 1. If the MasterName option is specified, a sentinel-backed FailoverClient is returned.
|
|
||||||
// 2. if the number of Addrs is two or more, a ClusterClient is returned.
|
|
||||||
// 3. Otherwise, a single-node Client is returned.
|
|
||||||
func NewUniversalClient(opts *UniversalOptions) UniversalClient {
|
|
||||||
if opts.MasterName != "" {
|
|
||||||
return NewFailoverClient(opts.Failover())
|
|
||||||
} else if len(opts.Addrs) > 1 {
|
|
||||||
return NewClusterClient(opts.Cluster())
|
|
||||||
}
|
|
||||||
return NewClient(opts.Simple())
|
|
||||||
}
|
|
@ -1,6 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
// Version is the current release version.
|
|
||||||
func Version() string {
|
|
||||||
return "8.11.5"
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2014 Chris Hines
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
@ -1,38 +0,0 @@
|
|||||||
[![GoDoc](https://godoc.org/github.com/go-stack/stack?status.svg)](https://godoc.org/github.com/go-stack/stack)
|
|
||||||
[![Go Report Card](https://goreportcard.com/badge/go-stack/stack)](https://goreportcard.com/report/go-stack/stack)
|
|
||||||
[![TravisCI](https://travis-ci.org/go-stack/stack.svg?branch=master)](https://travis-ci.org/go-stack/stack)
|
|
||||||
[![Coverage Status](https://coveralls.io/repos/github/go-stack/stack/badge.svg?branch=master)](https://coveralls.io/github/go-stack/stack?branch=master)
|
|
||||||
|
|
||||||
# stack
|
|
||||||
|
|
||||||
Package stack implements utilities to capture, manipulate, and format call
|
|
||||||
stacks. It provides a simpler API than package runtime.
|
|
||||||
|
|
||||||
The implementation takes care of the minutia and special cases of interpreting
|
|
||||||
the program counter (pc) values returned by runtime.Callers.
|
|
||||||
|
|
||||||
## Versioning
|
|
||||||
|
|
||||||
Package stack publishes releases via [semver](http://semver.org/) compatible Git
|
|
||||||
tags prefixed with a single 'v'. The master branch always contains the latest
|
|
||||||
release. The develop branch contains unreleased commits.
|
|
||||||
|
|
||||||
## Formatting
|
|
||||||
|
|
||||||
Package stack's types implement fmt.Formatter, which provides a simple and
|
|
||||||
flexible way to declaratively configure formatting when used with logging or
|
|
||||||
error tracking packages.
|
|
||||||
|
|
||||||
```go
|
|
||||||
func DoTheThing() {
|
|
||||||
c := stack.Caller(0)
|
|
||||||
log.Print(c) // "source.go:10"
|
|
||||||
log.Printf("%+v", c) // "pkg/path/source.go:10"
|
|
||||||
log.Printf("%n", c) // "DoTheThing"
|
|
||||||
|
|
||||||
s := stack.Trace().TrimRuntime()
|
|
||||||
log.Print(s) // "[source.go:15 caller.go:42 main.go:14]"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
See the docs for all of the supported formatting options.
|
|
@ -1,400 +0,0 @@
|
|||||||
// +build go1.7
|
|
||||||
|
|
||||||
// Package stack implements utilities to capture, manipulate, and format call
|
|
||||||
// stacks. It provides a simpler API than package runtime.
|
|
||||||
//
|
|
||||||
// The implementation takes care of the minutia and special cases of
|
|
||||||
// interpreting the program counter (pc) values returned by runtime.Callers.
|
|
||||||
//
|
|
||||||
// Package stack's types implement fmt.Formatter, which provides a simple and
|
|
||||||
// flexible way to declaratively configure formatting when used with logging
|
|
||||||
// or error tracking packages.
|
|
||||||
package stack
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Call records a single function invocation from a goroutine stack.
|
|
||||||
type Call struct {
|
|
||||||
frame runtime.Frame
|
|
||||||
}
|
|
||||||
|
|
||||||
// Caller returns a Call from the stack of the current goroutine. The argument
|
|
||||||
// skip is the number of stack frames to ascend, with 0 identifying the
|
|
||||||
// calling function.
|
|
||||||
func Caller(skip int) Call {
|
|
||||||
// As of Go 1.9 we need room for up to three PC entries.
|
|
||||||
//
|
|
||||||
// 0. An entry for the stack frame prior to the target to check for
|
|
||||||
// special handling needed if that prior entry is runtime.sigpanic.
|
|
||||||
// 1. A possible second entry to hold metadata about skipped inlined
|
|
||||||
// functions. If inline functions were not skipped the target frame
|
|
||||||
// PC will be here.
|
|
||||||
// 2. A third entry for the target frame PC when the second entry
|
|
||||||
// is used for skipped inline functions.
|
|
||||||
var pcs [3]uintptr
|
|
||||||
n := runtime.Callers(skip+1, pcs[:])
|
|
||||||
frames := runtime.CallersFrames(pcs[:n])
|
|
||||||
frame, _ := frames.Next()
|
|
||||||
frame, _ = frames.Next()
|
|
||||||
|
|
||||||
return Call{
|
|
||||||
frame: frame,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", c).
|
|
||||||
func (c Call) String() string {
|
|
||||||
return fmt.Sprint(c)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarshalText implements encoding.TextMarshaler. It formats the Call the same
|
|
||||||
// as fmt.Sprintf("%v", c).
|
|
||||||
func (c Call) MarshalText() ([]byte, error) {
|
|
||||||
if c.frame == (runtime.Frame{}) {
|
|
||||||
return nil, ErrNoFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
fmt.Fprint(&buf, c)
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrNoFunc means that the Call has a nil *runtime.Func. The most likely
|
|
||||||
// cause is a Call with the zero value.
|
|
||||||
var ErrNoFunc = errors.New("no call stack information")
|
|
||||||
|
|
||||||
// Format implements fmt.Formatter with support for the following verbs.
|
|
||||||
//
|
|
||||||
// %s source file
|
|
||||||
// %d line number
|
|
||||||
// %n function name
|
|
||||||
// %k last segment of the package path
|
|
||||||
// %v equivalent to %s:%d
|
|
||||||
//
|
|
||||||
// It accepts the '+' and '#' flags for most of the verbs as follows.
|
|
||||||
//
|
|
||||||
// %+s path of source file relative to the compile time GOPATH,
|
|
||||||
// or the module path joined to the path of source file relative
|
|
||||||
// to module root
|
|
||||||
// %#s full path of source file
|
|
||||||
// %+n import path qualified function name
|
|
||||||
// %+k full package path
|
|
||||||
// %+v equivalent to %+s:%d
|
|
||||||
// %#v equivalent to %#s:%d
|
|
||||||
func (c Call) Format(s fmt.State, verb rune) {
|
|
||||||
if c.frame == (runtime.Frame{}) {
|
|
||||||
fmt.Fprintf(s, "%%!%c(NOFUNC)", verb)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch verb {
|
|
||||||
case 's', 'v':
|
|
||||||
file := c.frame.File
|
|
||||||
switch {
|
|
||||||
case s.Flag('#'):
|
|
||||||
// done
|
|
||||||
case s.Flag('+'):
|
|
||||||
file = pkgFilePath(&c.frame)
|
|
||||||
default:
|
|
||||||
const sep = "/"
|
|
||||||
if i := strings.LastIndex(file, sep); i != -1 {
|
|
||||||
file = file[i+len(sep):]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
io.WriteString(s, file)
|
|
||||||
if verb == 'v' {
|
|
||||||
buf := [7]byte{':'}
|
|
||||||
s.Write(strconv.AppendInt(buf[:1], int64(c.frame.Line), 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'd':
|
|
||||||
buf := [6]byte{}
|
|
||||||
s.Write(strconv.AppendInt(buf[:0], int64(c.frame.Line), 10))
|
|
||||||
|
|
||||||
case 'k':
|
|
||||||
name := c.frame.Function
|
|
||||||
const pathSep = "/"
|
|
||||||
start, end := 0, len(name)
|
|
||||||
if i := strings.LastIndex(name, pathSep); i != -1 {
|
|
||||||
start = i + len(pathSep)
|
|
||||||
}
|
|
||||||
const pkgSep = "."
|
|
||||||
if i := strings.Index(name[start:], pkgSep); i != -1 {
|
|
||||||
end = start + i
|
|
||||||
}
|
|
||||||
if s.Flag('+') {
|
|
||||||
start = 0
|
|
||||||
}
|
|
||||||
io.WriteString(s, name[start:end])
|
|
||||||
|
|
||||||
case 'n':
|
|
||||||
name := c.frame.Function
|
|
||||||
if !s.Flag('+') {
|
|
||||||
const pathSep = "/"
|
|
||||||
if i := strings.LastIndex(name, pathSep); i != -1 {
|
|
||||||
name = name[i+len(pathSep):]
|
|
||||||
}
|
|
||||||
const pkgSep = "."
|
|
||||||
if i := strings.Index(name, pkgSep); i != -1 {
|
|
||||||
name = name[i+len(pkgSep):]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
io.WriteString(s, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Frame returns the call frame infomation for the Call.
|
|
||||||
func (c Call) Frame() runtime.Frame {
|
|
||||||
return c.frame
|
|
||||||
}
|
|
||||||
|
|
||||||
// PC returns the program counter for this call frame; multiple frames may
|
|
||||||
// have the same PC value.
|
|
||||||
//
|
|
||||||
// Deprecated: Use Call.Frame instead.
|
|
||||||
func (c Call) PC() uintptr {
|
|
||||||
return c.frame.PC
|
|
||||||
}
|
|
||||||
|
|
||||||
// CallStack records a sequence of function invocations from a goroutine
|
|
||||||
// stack.
|
|
||||||
type CallStack []Call
|
|
||||||
|
|
||||||
// String implements fmt.Stinger. It is equivalent to fmt.Sprintf("%v", cs).
|
|
||||||
func (cs CallStack) String() string {
|
|
||||||
return fmt.Sprint(cs)
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
openBracketBytes = []byte("[")
|
|
||||||
closeBracketBytes = []byte("]")
|
|
||||||
spaceBytes = []byte(" ")
|
|
||||||
)
|
|
||||||
|
|
||||||
// MarshalText implements encoding.TextMarshaler. It formats the CallStack the
|
|
||||||
// same as fmt.Sprintf("%v", cs).
|
|
||||||
func (cs CallStack) MarshalText() ([]byte, error) {
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
buf.Write(openBracketBytes)
|
|
||||||
for i, pc := range cs {
|
|
||||||
if i > 0 {
|
|
||||||
buf.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
fmt.Fprint(&buf, pc)
|
|
||||||
}
|
|
||||||
buf.Write(closeBracketBytes)
|
|
||||||
return buf.Bytes(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format implements fmt.Formatter by printing the CallStack as square brackets
|
|
||||||
// ([, ]) surrounding a space separated list of Calls each formatted with the
|
|
||||||
// supplied verb and options.
|
|
||||||
func (cs CallStack) Format(s fmt.State, verb rune) {
|
|
||||||
s.Write(openBracketBytes)
|
|
||||||
for i, pc := range cs {
|
|
||||||
if i > 0 {
|
|
||||||
s.Write(spaceBytes)
|
|
||||||
}
|
|
||||||
pc.Format(s, verb)
|
|
||||||
}
|
|
||||||
s.Write(closeBracketBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trace returns a CallStack for the current goroutine with element 0
|
|
||||||
// identifying the calling function.
|
|
||||||
func Trace() CallStack {
|
|
||||||
var pcs [512]uintptr
|
|
||||||
n := runtime.Callers(1, pcs[:])
|
|
||||||
|
|
||||||
frames := runtime.CallersFrames(pcs[:n])
|
|
||||||
cs := make(CallStack, 0, n)
|
|
||||||
|
|
||||||
// Skip extra frame retrieved just to make sure the runtime.sigpanic
|
|
||||||
// special case is handled.
|
|
||||||
frame, more := frames.Next()
|
|
||||||
|
|
||||||
for more {
|
|
||||||
frame, more = frames.Next()
|
|
||||||
cs = append(cs, Call{frame: frame})
|
|
||||||
}
|
|
||||||
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrimBelow returns a slice of the CallStack with all entries below c
|
|
||||||
// removed.
|
|
||||||
func (cs CallStack) TrimBelow(c Call) CallStack {
|
|
||||||
for len(cs) > 0 && cs[0] != c {
|
|
||||||
cs = cs[1:]
|
|
||||||
}
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrimAbove returns a slice of the CallStack with all entries above c
|
|
||||||
// removed.
|
|
||||||
func (cs CallStack) TrimAbove(c Call) CallStack {
|
|
||||||
for len(cs) > 0 && cs[len(cs)-1] != c {
|
|
||||||
cs = cs[:len(cs)-1]
|
|
||||||
}
|
|
||||||
return cs
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkgIndex returns the index that results in file[index:] being the path of
|
|
||||||
// file relative to the compile time GOPATH, and file[:index] being the
|
|
||||||
// $GOPATH/src/ portion of file. funcName must be the name of a function in
|
|
||||||
// file as returned by runtime.Func.Name.
|
|
||||||
func pkgIndex(file, funcName string) int {
|
|
||||||
// As of Go 1.6.2 there is no direct way to know the compile time GOPATH
|
|
||||||
// at runtime, but we can infer the number of path segments in the GOPATH.
|
|
||||||
// We note that runtime.Func.Name() returns the function name qualified by
|
|
||||||
// the import path, which does not include the GOPATH. Thus we can trim
|
|
||||||
// segments from the beginning of the file path until the number of path
|
|
||||||
// separators remaining is one more than the number of path separators in
|
|
||||||
// the function name. For example, given:
|
|
||||||
//
|
|
||||||
// GOPATH /home/user
|
|
||||||
// file /home/user/src/pkg/sub/file.go
|
|
||||||
// fn.Name() pkg/sub.Type.Method
|
|
||||||
//
|
|
||||||
// We want to produce:
|
|
||||||
//
|
|
||||||
// file[:idx] == /home/user/src/
|
|
||||||
// file[idx:] == pkg/sub/file.go
|
|
||||||
//
|
|
||||||
// From this we can easily see that fn.Name() has one less path separator
|
|
||||||
// than our desired result for file[idx:]. We count separators from the
|
|
||||||
// end of the file path until it finds two more than in the function name
|
|
||||||
// and then move one character forward to preserve the initial path
|
|
||||||
// segment without a leading separator.
|
|
||||||
const sep = "/"
|
|
||||||
i := len(file)
|
|
||||||
for n := strings.Count(funcName, sep) + 2; n > 0; n-- {
|
|
||||||
i = strings.LastIndex(file[:i], sep)
|
|
||||||
if i == -1 {
|
|
||||||
i = -len(sep)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// get back to 0 or trim the leading separator
|
|
||||||
return i + len(sep)
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkgFilePath returns the frame's filepath relative to the compile-time GOPATH,
|
|
||||||
// or its module path joined to its path relative to the module root.
|
|
||||||
//
|
|
||||||
// As of Go 1.11 there is no direct way to know the compile time GOPATH or
|
|
||||||
// module paths at runtime, but we can piece together the desired information
|
|
||||||
// from available information. We note that runtime.Frame.Function contains the
|
|
||||||
// function name qualified by the package path, which includes the module path
|
|
||||||
// but not the GOPATH. We can extract the package path from that and append the
|
|
||||||
// last segments of the file path to arrive at the desired package qualified
|
|
||||||
// file path. For example, given:
|
|
||||||
//
|
|
||||||
// GOPATH /home/user
|
|
||||||
// import path pkg/sub
|
|
||||||
// frame.File /home/user/src/pkg/sub/file.go
|
|
||||||
// frame.Function pkg/sub.Type.Method
|
|
||||||
// Desired return pkg/sub/file.go
|
|
||||||
//
|
|
||||||
// It appears that we simply need to trim ".Type.Method" from frame.Function and
|
|
||||||
// append "/" + path.Base(file).
|
|
||||||
//
|
|
||||||
// But there are other wrinkles. Although it is idiomatic to do so, the internal
|
|
||||||
// name of a package is not required to match the last segment of its import
|
|
||||||
// path. In addition, the introduction of modules in Go 1.11 allows working
|
|
||||||
// without a GOPATH. So we also must make these work right:
|
|
||||||
//
|
|
||||||
// GOPATH /home/user
|
|
||||||
// import path pkg/go-sub
|
|
||||||
// package name sub
|
|
||||||
// frame.File /home/user/src/pkg/go-sub/file.go
|
|
||||||
// frame.Function pkg/sub.Type.Method
|
|
||||||
// Desired return pkg/go-sub/file.go
|
|
||||||
//
|
|
||||||
// Module path pkg/v2
|
|
||||||
// import path pkg/v2/go-sub
|
|
||||||
// package name sub
|
|
||||||
// frame.File /home/user/cloned-pkg/go-sub/file.go
|
|
||||||
// frame.Function pkg/v2/sub.Type.Method
|
|
||||||
// Desired return pkg/v2/go-sub/file.go
|
|
||||||
//
|
|
||||||
// We can handle all of these situations by using the package path extracted
|
|
||||||
// from frame.Function up to, but not including, the last segment as the prefix
|
|
||||||
// and the last two segments of frame.File as the suffix of the returned path.
|
|
||||||
// This preserves the existing behavior when working in a GOPATH without modules
|
|
||||||
// and a semantically equivalent behavior when used in module aware project.
|
|
||||||
func pkgFilePath(frame *runtime.Frame) string {
|
|
||||||
pre := pkgPrefix(frame.Function)
|
|
||||||
post := pathSuffix(frame.File)
|
|
||||||
if pre == "" {
|
|
||||||
return post
|
|
||||||
}
|
|
||||||
return pre + "/" + post
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkgPrefix returns the import path of the function's package with the final
|
|
||||||
// segment removed.
|
|
||||||
func pkgPrefix(funcName string) string {
|
|
||||||
const pathSep = "/"
|
|
||||||
end := strings.LastIndex(funcName, pathSep)
|
|
||||||
if end == -1 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return funcName[:end]
|
|
||||||
}
|
|
||||||
|
|
||||||
// pathSuffix returns the last two segments of path.
|
|
||||||
func pathSuffix(path string) string {
|
|
||||||
const pathSep = "/"
|
|
||||||
lastSep := strings.LastIndex(path, pathSep)
|
|
||||||
if lastSep == -1 {
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return path[strings.LastIndex(path[:lastSep], pathSep)+1:]
|
|
||||||
}
|
|
||||||
|
|
||||||
var runtimePath string
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var pcs [3]uintptr
|
|
||||||
runtime.Callers(0, pcs[:])
|
|
||||||
frames := runtime.CallersFrames(pcs[:])
|
|
||||||
frame, _ := frames.Next()
|
|
||||||
file := frame.File
|
|
||||||
|
|
||||||
idx := pkgIndex(frame.File, frame.Function)
|
|
||||||
|
|
||||||
runtimePath = file[:idx]
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
runtimePath = strings.ToLower(runtimePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func inGoroot(c Call) bool {
|
|
||||||
file := c.frame.File
|
|
||||||
if len(file) == 0 || file[0] == '?' {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
file = strings.ToLower(file)
|
|
||||||
}
|
|
||||||
return strings.HasPrefix(file, runtimePath) || strings.HasSuffix(file, "/_testmain.go")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TrimRuntime returns a slice of the CallStack with the topmost entries from
|
|
||||||
// the go runtime removed. It considers any calls originating from unknown
|
|
||||||
// files, files under GOROOT, or _testmain.go as part of the runtime.
|
|
||||||
func (cs CallStack) TrimRuntime() CallStack {
|
|
||||||
for len(cs) > 0 && inGoroot(cs[len(cs)-1]) {
|
|
||||||
cs = cs[:len(cs)-1]
|
|
||||||
}
|
|
||||||
return cs
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
cmd/snappytool/snappytool
|
|
||||||
testdata/bench
|
|
||||||
|
|
||||||
# These explicitly listed benchmark data files are for an obsolete version of
|
|
||||||
# snappy_test.go.
|
|
||||||
testdata/alice29.txt
|
|
||||||
testdata/asyoulik.txt
|
|
||||||
testdata/fireworks.jpeg
|
|
||||||
testdata/geo.protodata
|
|
||||||
testdata/html
|
|
||||||
testdata/html_x_4
|
|
||||||
testdata/kppkn.gtb
|
|
||||||
testdata/lcet10.txt
|
|
||||||
testdata/paper-100k.pdf
|
|
||||||
testdata/plrabn12.txt
|
|
||||||
testdata/urls.10K
|
|
@ -1,18 +0,0 @@
|
|||||||
# This is the official list of Snappy-Go authors for copyright purposes.
|
|
||||||
# This file is distinct from the CONTRIBUTORS files.
|
|
||||||
# See the latter for an explanation.
|
|
||||||
|
|
||||||
# Names should be added to this file as
|
|
||||||
# Name or Organization <email address>
|
|
||||||
# The email address is not required for organizations.
|
|
||||||
|
|
||||||
# Please keep the list sorted.
|
|
||||||
|
|
||||||
Amazon.com, Inc
|
|
||||||
Damian Gryski <dgryski@gmail.com>
|
|
||||||
Eric Buth <eric@topos.com>
|
|
||||||
Google Inc.
|
|
||||||
Jan Mercl <0xjnml@gmail.com>
|
|
||||||
Klaus Post <klauspost@gmail.com>
|
|
||||||
Rodolfo Carvalho <rhcarvalho@gmail.com>
|
|
||||||
Sebastien Binet <seb.binet@gmail.com>
|
|
@ -1,41 +0,0 @@
|
|||||||
# This is the official list of people who can contribute
|
|
||||||
# (and typically have contributed) code to the Snappy-Go repository.
|
|
||||||
# The AUTHORS file lists the copyright holders; this file
|
|
||||||
# lists people. For example, Google employees are listed here
|
|
||||||
# but not in AUTHORS, because Google holds the copyright.
|
|
||||||
#
|
|
||||||
# The submission process automatically checks to make sure
|
|
||||||
# that people submitting code are listed in this file (by email address).
|
|
||||||
#
|
|
||||||
# Names should be added to this file only after verifying that
|
|
||||||
# the individual or the individual's organization has agreed to
|
|
||||||
# the appropriate Contributor License Agreement, found here:
|
|
||||||
#
|
|
||||||
# http://code.google.com/legal/individual-cla-v1.0.html
|
|
||||||
# http://code.google.com/legal/corporate-cla-v1.0.html
|
|
||||||
#
|
|
||||||
# The agreement for individuals can be filled out on the web.
|
|
||||||
#
|
|
||||||
# When adding J Random Contributor's name to this file,
|
|
||||||
# either J's name or J's organization's name should be
|
|
||||||
# added to the AUTHORS file, depending on whether the
|
|
||||||
# individual or corporate CLA was used.
|
|
||||||
|
|
||||||
# Names should be added to this file like so:
|
|
||||||
# Name <email address>
|
|
||||||
|
|
||||||
# Please keep the list sorted.
|
|
||||||
|
|
||||||
Alex Legg <alexlegg@google.com>
|
|
||||||
Damian Gryski <dgryski@gmail.com>
|
|
||||||
Eric Buth <eric@topos.com>
|
|
||||||
Jan Mercl <0xjnml@gmail.com>
|
|
||||||
Jonathan Swinney <jswinney@amazon.com>
|
|
||||||
Kai Backman <kaib@golang.org>
|
|
||||||
Klaus Post <klauspost@gmail.com>
|
|
||||||
Marc-Antoine Ruel <maruel@chromium.org>
|
|
||||||
Nigel Tao <nigeltao@golang.org>
|
|
||||||
Rob Pike <r@golang.org>
|
|
||||||
Rodolfo Carvalho <rhcarvalho@gmail.com>
|
|
||||||
Russ Cox <rsc@golang.org>
|
|
||||||
Sebastien Binet <seb.binet@gmail.com>
|
|
@ -1,27 +0,0 @@
|
|||||||
Copyright (c) 2011 The Snappy-Go Authors. All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following disclaimer
|
|
||||||
in the documentation and/or other materials provided with the
|
|
||||||
distribution.
|
|
||||||
* Neither the name of Google Inc. nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived from
|
|
||||||
this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
||||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
||||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
||||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
||||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
||||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,107 +0,0 @@
|
|||||||
The Snappy compression format in the Go programming language.
|
|
||||||
|
|
||||||
To download and install from source:
|
|
||||||
$ go get github.com/golang/snappy
|
|
||||||
|
|
||||||
Unless otherwise noted, the Snappy-Go source files are distributed
|
|
||||||
under the BSD-style license found in the LICENSE file.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Benchmarks.
|
|
||||||
|
|
||||||
The golang/snappy benchmarks include compressing (Z) and decompressing (U) ten
|
|
||||||
or so files, the same set used by the C++ Snappy code (github.com/google/snappy
|
|
||||||
and note the "google", not "golang"). On an "Intel(R) Core(TM) i7-3770 CPU @
|
|
||||||
3.40GHz", Go's GOARCH=amd64 numbers as of 2016-05-29:
|
|
||||||
|
|
||||||
"go test -test.bench=."
|
|
||||||
|
|
||||||
_UFlat0-8 2.19GB/s ± 0% html
|
|
||||||
_UFlat1-8 1.41GB/s ± 0% urls
|
|
||||||
_UFlat2-8 23.5GB/s ± 2% jpg
|
|
||||||
_UFlat3-8 1.91GB/s ± 0% jpg_200
|
|
||||||
_UFlat4-8 14.0GB/s ± 1% pdf
|
|
||||||
_UFlat5-8 1.97GB/s ± 0% html4
|
|
||||||
_UFlat6-8 814MB/s ± 0% txt1
|
|
||||||
_UFlat7-8 785MB/s ± 0% txt2
|
|
||||||
_UFlat8-8 857MB/s ± 0% txt3
|
|
||||||
_UFlat9-8 719MB/s ± 1% txt4
|
|
||||||
_UFlat10-8 2.84GB/s ± 0% pb
|
|
||||||
_UFlat11-8 1.05GB/s ± 0% gaviota
|
|
||||||
|
|
||||||
_ZFlat0-8 1.04GB/s ± 0% html
|
|
||||||
_ZFlat1-8 534MB/s ± 0% urls
|
|
||||||
_ZFlat2-8 15.7GB/s ± 1% jpg
|
|
||||||
_ZFlat3-8 740MB/s ± 3% jpg_200
|
|
||||||
_ZFlat4-8 9.20GB/s ± 1% pdf
|
|
||||||
_ZFlat5-8 991MB/s ± 0% html4
|
|
||||||
_ZFlat6-8 379MB/s ± 0% txt1
|
|
||||||
_ZFlat7-8 352MB/s ± 0% txt2
|
|
||||||
_ZFlat8-8 396MB/s ± 1% txt3
|
|
||||||
_ZFlat9-8 327MB/s ± 1% txt4
|
|
||||||
_ZFlat10-8 1.33GB/s ± 1% pb
|
|
||||||
_ZFlat11-8 605MB/s ± 1% gaviota
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"go test -test.bench=. -tags=noasm"
|
|
||||||
|
|
||||||
_UFlat0-8 621MB/s ± 2% html
|
|
||||||
_UFlat1-8 494MB/s ± 1% urls
|
|
||||||
_UFlat2-8 23.2GB/s ± 1% jpg
|
|
||||||
_UFlat3-8 1.12GB/s ± 1% jpg_200
|
|
||||||
_UFlat4-8 4.35GB/s ± 1% pdf
|
|
||||||
_UFlat5-8 609MB/s ± 0% html4
|
|
||||||
_UFlat6-8 296MB/s ± 0% txt1
|
|
||||||
_UFlat7-8 288MB/s ± 0% txt2
|
|
||||||
_UFlat8-8 309MB/s ± 1% txt3
|
|
||||||
_UFlat9-8 280MB/s ± 1% txt4
|
|
||||||
_UFlat10-8 753MB/s ± 0% pb
|
|
||||||
_UFlat11-8 400MB/s ± 0% gaviota
|
|
||||||
|
|
||||||
_ZFlat0-8 409MB/s ± 1% html
|
|
||||||
_ZFlat1-8 250MB/s ± 1% urls
|
|
||||||
_ZFlat2-8 12.3GB/s ± 1% jpg
|
|
||||||
_ZFlat3-8 132MB/s ± 0% jpg_200
|
|
||||||
_ZFlat4-8 2.92GB/s ± 0% pdf
|
|
||||||
_ZFlat5-8 405MB/s ± 1% html4
|
|
||||||
_ZFlat6-8 179MB/s ± 1% txt1
|
|
||||||
_ZFlat7-8 170MB/s ± 1% txt2
|
|
||||||
_ZFlat8-8 189MB/s ± 1% txt3
|
|
||||||
_ZFlat9-8 164MB/s ± 1% txt4
|
|
||||||
_ZFlat10-8 479MB/s ± 1% pb
|
|
||||||
_ZFlat11-8 270MB/s ± 1% gaviota
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
For comparison (Go's encoded output is byte-for-byte identical to C++'s), here
|
|
||||||
are the numbers from C++ Snappy's
|
|
||||||
|
|
||||||
make CXXFLAGS="-O2 -DNDEBUG -g" clean snappy_unittest.log && cat snappy_unittest.log
|
|
||||||
|
|
||||||
BM_UFlat/0 2.4GB/s html
|
|
||||||
BM_UFlat/1 1.4GB/s urls
|
|
||||||
BM_UFlat/2 21.8GB/s jpg
|
|
||||||
BM_UFlat/3 1.5GB/s jpg_200
|
|
||||||
BM_UFlat/4 13.3GB/s pdf
|
|
||||||
BM_UFlat/5 2.1GB/s html4
|
|
||||||
BM_UFlat/6 1.0GB/s txt1
|
|
||||||
BM_UFlat/7 959.4MB/s txt2
|
|
||||||
BM_UFlat/8 1.0GB/s txt3
|
|
||||||
BM_UFlat/9 864.5MB/s txt4
|
|
||||||
BM_UFlat/10 2.9GB/s pb
|
|
||||||
BM_UFlat/11 1.2GB/s gaviota
|
|
||||||
|
|
||||||
BM_ZFlat/0 944.3MB/s html (22.31 %)
|
|
||||||
BM_ZFlat/1 501.6MB/s urls (47.78 %)
|
|
||||||
BM_ZFlat/2 14.3GB/s jpg (99.95 %)
|
|
||||||
BM_ZFlat/3 538.3MB/s jpg_200 (73.00 %)
|
|
||||||
BM_ZFlat/4 8.3GB/s pdf (83.30 %)
|
|
||||||
BM_ZFlat/5 903.5MB/s html4 (22.52 %)
|
|
||||||
BM_ZFlat/6 336.0MB/s txt1 (57.88 %)
|
|
||||||
BM_ZFlat/7 312.3MB/s txt2 (61.91 %)
|
|
||||||
BM_ZFlat/8 353.1MB/s txt3 (54.99 %)
|
|
||||||
BM_ZFlat/9 289.9MB/s txt4 (66.26 %)
|
|
||||||
BM_ZFlat/10 1.2GB/s pb (19.68 %)
|
|
||||||
BM_ZFlat/11 527.4MB/s gaviota (37.72 %)
|
|
@ -1,264 +0,0 @@
|
|||||||
// Copyright 2011 The Snappy-Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package snappy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrCorrupt reports that the input is invalid.
|
|
||||||
ErrCorrupt = errors.New("snappy: corrupt input")
|
|
||||||
// ErrTooLarge reports that the uncompressed length is too large.
|
|
||||||
ErrTooLarge = errors.New("snappy: decoded block is too large")
|
|
||||||
// ErrUnsupported reports that the input isn't supported.
|
|
||||||
ErrUnsupported = errors.New("snappy: unsupported input")
|
|
||||||
|
|
||||||
errUnsupportedLiteralLength = errors.New("snappy: unsupported literal length")
|
|
||||||
)
|
|
||||||
|
|
||||||
// DecodedLen returns the length of the decoded block.
|
|
||||||
func DecodedLen(src []byte) (int, error) {
|
|
||||||
v, _, err := decodedLen(src)
|
|
||||||
return v, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// decodedLen returns the length of the decoded block and the number of bytes
|
|
||||||
// that the length header occupied.
|
|
||||||
func decodedLen(src []byte) (blockLen, headerLen int, err error) {
|
|
||||||
v, n := binary.Uvarint(src)
|
|
||||||
if n <= 0 || v > 0xffffffff {
|
|
||||||
return 0, 0, ErrCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
const wordSize = 32 << (^uint(0) >> 32 & 1)
|
|
||||||
if wordSize == 32 && v > 0x7fffffff {
|
|
||||||
return 0, 0, ErrTooLarge
|
|
||||||
}
|
|
||||||
return int(v), n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
decodeErrCodeCorrupt = 1
|
|
||||||
decodeErrCodeUnsupportedLiteralLength = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
// Decode returns the decoded form of src. The returned slice may be a sub-
|
|
||||||
// slice of dst if dst was large enough to hold the entire decoded block.
|
|
||||||
// Otherwise, a newly allocated slice will be returned.
|
|
||||||
//
|
|
||||||
// The dst and src must not overlap. It is valid to pass a nil dst.
|
|
||||||
//
|
|
||||||
// Decode handles the Snappy block format, not the Snappy stream format.
|
|
||||||
func Decode(dst, src []byte) ([]byte, error) {
|
|
||||||
dLen, s, err := decodedLen(src)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if dLen <= len(dst) {
|
|
||||||
dst = dst[:dLen]
|
|
||||||
} else {
|
|
||||||
dst = make([]byte, dLen)
|
|
||||||
}
|
|
||||||
switch decode(dst, src[s:]) {
|
|
||||||
case 0:
|
|
||||||
return dst, nil
|
|
||||||
case decodeErrCodeUnsupportedLiteralLength:
|
|
||||||
return nil, errUnsupportedLiteralLength
|
|
||||||
}
|
|
||||||
return nil, ErrCorrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewReader returns a new Reader that decompresses from r, using the framing
|
|
||||||
// format described at
|
|
||||||
// https://github.com/google/snappy/blob/master/framing_format.txt
|
|
||||||
func NewReader(r io.Reader) *Reader {
|
|
||||||
return &Reader{
|
|
||||||
r: r,
|
|
||||||
decoded: make([]byte, maxBlockSize),
|
|
||||||
buf: make([]byte, maxEncodedLenOfMaxBlockSize+checksumSize),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reader is an io.Reader that can read Snappy-compressed bytes.
|
|
||||||
//
|
|
||||||
// Reader handles the Snappy stream format, not the Snappy block format.
|
|
||||||
type Reader struct {
|
|
||||||
r io.Reader
|
|
||||||
err error
|
|
||||||
decoded []byte
|
|
||||||
buf []byte
|
|
||||||
// decoded[i:j] contains decoded bytes that have not yet been passed on.
|
|
||||||
i, j int
|
|
||||||
readHeader bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset discards any buffered data, resets all state, and switches the Snappy
|
|
||||||
// reader to read from r. This permits reusing a Reader rather than allocating
|
|
||||||
// a new one.
|
|
||||||
func (r *Reader) Reset(reader io.Reader) {
|
|
||||||
r.r = reader
|
|
||||||
r.err = nil
|
|
||||||
r.i = 0
|
|
||||||
r.j = 0
|
|
||||||
r.readHeader = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) readFull(p []byte, allowEOF bool) (ok bool) {
|
|
||||||
if _, r.err = io.ReadFull(r.r, p); r.err != nil {
|
|
||||||
if r.err == io.ErrUnexpectedEOF || (r.err == io.EOF && !allowEOF) {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Reader) fill() error {
|
|
||||||
for r.i >= r.j {
|
|
||||||
if !r.readFull(r.buf[:4], true) {
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
chunkType := r.buf[0]
|
|
||||||
if !r.readHeader {
|
|
||||||
if chunkType != chunkTypeStreamIdentifier {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
r.readHeader = true
|
|
||||||
}
|
|
||||||
chunkLen := int(r.buf[1]) | int(r.buf[2])<<8 | int(r.buf[3])<<16
|
|
||||||
if chunkLen > len(r.buf) {
|
|
||||||
r.err = ErrUnsupported
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The chunk types are specified at
|
|
||||||
// https://github.com/google/snappy/blob/master/framing_format.txt
|
|
||||||
switch chunkType {
|
|
||||||
case chunkTypeCompressedData:
|
|
||||||
// Section 4.2. Compressed data (chunk type 0x00).
|
|
||||||
if chunkLen < checksumSize {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
buf := r.buf[:chunkLen]
|
|
||||||
if !r.readFull(buf, false) {
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
checksum := uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24
|
|
||||||
buf = buf[checksumSize:]
|
|
||||||
|
|
||||||
n, err := DecodedLen(buf)
|
|
||||||
if err != nil {
|
|
||||||
r.err = err
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
if n > len(r.decoded) {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
if _, err := Decode(r.decoded, buf); err != nil {
|
|
||||||
r.err = err
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
if crc(r.decoded[:n]) != checksum {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
r.i, r.j = 0, n
|
|
||||||
continue
|
|
||||||
|
|
||||||
case chunkTypeUncompressedData:
|
|
||||||
// Section 4.3. Uncompressed data (chunk type 0x01).
|
|
||||||
if chunkLen < checksumSize {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
buf := r.buf[:checksumSize]
|
|
||||||
if !r.readFull(buf, false) {
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
checksum := uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24
|
|
||||||
// Read directly into r.decoded instead of via r.buf.
|
|
||||||
n := chunkLen - checksumSize
|
|
||||||
if n > len(r.decoded) {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
if !r.readFull(r.decoded[:n], false) {
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
if crc(r.decoded[:n]) != checksum {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
r.i, r.j = 0, n
|
|
||||||
continue
|
|
||||||
|
|
||||||
case chunkTypeStreamIdentifier:
|
|
||||||
// Section 4.1. Stream identifier (chunk type 0xff).
|
|
||||||
if chunkLen != len(magicBody) {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
if !r.readFull(r.buf[:len(magicBody)], false) {
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
for i := 0; i < len(magicBody); i++ {
|
|
||||||
if r.buf[i] != magicBody[i] {
|
|
||||||
r.err = ErrCorrupt
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if chunkType <= 0x7f {
|
|
||||||
// Section 4.5. Reserved unskippable chunks (chunk types 0x02-0x7f).
|
|
||||||
r.err = ErrUnsupported
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
// Section 4.4 Padding (chunk type 0xfe).
|
|
||||||
// Section 4.6. Reserved skippable chunks (chunk types 0x80-0xfd).
|
|
||||||
if !r.readFull(r.buf[:chunkLen], false) {
|
|
||||||
return r.err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read satisfies the io.Reader interface.
|
|
||||||
func (r *Reader) Read(p []byte) (int, error) {
|
|
||||||
if r.err != nil {
|
|
||||||
return 0, r.err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.fill(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n := copy(p, r.decoded[r.i:r.j])
|
|
||||||
r.i += n
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadByte satisfies the io.ByteReader interface.
|
|
||||||
func (r *Reader) ReadByte() (byte, error) {
|
|
||||||
if r.err != nil {
|
|
||||||
return 0, r.err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := r.fill(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
c := r.decoded[r.i]
|
|
||||||
r.i++
|
|
||||||
return c, nil
|
|
||||||
}
|
|
@ -1,490 +0,0 @@
|
|||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !appengine
|
|
||||||
// +build gc
|
|
||||||
// +build !noasm
|
|
||||||
|
|
||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// The asm code generally follows the pure Go code in decode_other.go, except
|
|
||||||
// where marked with a "!!!".
|
|
||||||
|
|
||||||
// func decode(dst, src []byte) int
|
|
||||||
//
|
|
||||||
// All local variables fit into registers. The non-zero stack size is only to
|
|
||||||
// spill registers and push args when issuing a CALL. The register allocation:
|
|
||||||
// - AX scratch
|
|
||||||
// - BX scratch
|
|
||||||
// - CX length or x
|
|
||||||
// - DX offset
|
|
||||||
// - SI &src[s]
|
|
||||||
// - DI &dst[d]
|
|
||||||
// + R8 dst_base
|
|
||||||
// + R9 dst_len
|
|
||||||
// + R10 dst_base + dst_len
|
|
||||||
// + R11 src_base
|
|
||||||
// + R12 src_len
|
|
||||||
// + R13 src_base + src_len
|
|
||||||
// - R14 used by doCopy
|
|
||||||
// - R15 used by doCopy
|
|
||||||
//
|
|
||||||
// The registers R8-R13 (marked with a "+") are set at the start of the
|
|
||||||
// function, and after a CALL returns, and are not otherwise modified.
|
|
||||||
//
|
|
||||||
// The d variable is implicitly DI - R8, and len(dst)-d is R10 - DI.
|
|
||||||
// The s variable is implicitly SI - R11, and len(src)-s is R13 - SI.
|
|
||||||
TEXT ·decode(SB), NOSPLIT, $48-56
|
|
||||||
// Initialize SI, DI and R8-R13.
|
|
||||||
MOVQ dst_base+0(FP), R8
|
|
||||||
MOVQ dst_len+8(FP), R9
|
|
||||||
MOVQ R8, DI
|
|
||||||
MOVQ R8, R10
|
|
||||||
ADDQ R9, R10
|
|
||||||
MOVQ src_base+24(FP), R11
|
|
||||||
MOVQ src_len+32(FP), R12
|
|
||||||
MOVQ R11, SI
|
|
||||||
MOVQ R11, R13
|
|
||||||
ADDQ R12, R13
|
|
||||||
|
|
||||||
loop:
|
|
||||||
// for s < len(src)
|
|
||||||
CMPQ SI, R13
|
|
||||||
JEQ end
|
|
||||||
|
|
||||||
// CX = uint32(src[s])
|
|
||||||
//
|
|
||||||
// switch src[s] & 0x03
|
|
||||||
MOVBLZX (SI), CX
|
|
||||||
MOVL CX, BX
|
|
||||||
ANDL $3, BX
|
|
||||||
CMPL BX, $1
|
|
||||||
JAE tagCopy
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// The code below handles literal tags.
|
|
||||||
|
|
||||||
// case tagLiteral:
|
|
||||||
// x := uint32(src[s] >> 2)
|
|
||||||
// switch
|
|
||||||
SHRL $2, CX
|
|
||||||
CMPL CX, $60
|
|
||||||
JAE tagLit60Plus
|
|
||||||
|
|
||||||
// case x < 60:
|
|
||||||
// s++
|
|
||||||
INCQ SI
|
|
||||||
|
|
||||||
doLit:
|
|
||||||
// This is the end of the inner "switch", when we have a literal tag.
|
|
||||||
//
|
|
||||||
// We assume that CX == x and x fits in a uint32, where x is the variable
|
|
||||||
// used in the pure Go decode_other.go code.
|
|
||||||
|
|
||||||
// length = int(x) + 1
|
|
||||||
//
|
|
||||||
// Unlike the pure Go code, we don't need to check if length <= 0 because
|
|
||||||
// CX can hold 64 bits, so the increment cannot overflow.
|
|
||||||
INCQ CX
|
|
||||||
|
|
||||||
// Prepare to check if copying length bytes will run past the end of dst or
|
|
||||||
// src.
|
|
||||||
//
|
|
||||||
// AX = len(dst) - d
|
|
||||||
// BX = len(src) - s
|
|
||||||
MOVQ R10, AX
|
|
||||||
SUBQ DI, AX
|
|
||||||
MOVQ R13, BX
|
|
||||||
SUBQ SI, BX
|
|
||||||
|
|
||||||
// !!! Try a faster technique for short (16 or fewer bytes) copies.
|
|
||||||
//
|
|
||||||
// if length > 16 || len(dst)-d < 16 || len(src)-s < 16 {
|
|
||||||
// goto callMemmove // Fall back on calling runtime·memmove.
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// The C++ snappy code calls this TryFastAppend. It also checks len(src)-s
|
|
||||||
// against 21 instead of 16, because it cannot assume that all of its input
|
|
||||||
// is contiguous in memory and so it needs to leave enough source bytes to
|
|
||||||
// read the next tag without refilling buffers, but Go's Decode assumes
|
|
||||||
// contiguousness (the src argument is a []byte).
|
|
||||||
CMPQ CX, $16
|
|
||||||
JGT callMemmove
|
|
||||||
CMPQ AX, $16
|
|
||||||
JLT callMemmove
|
|
||||||
CMPQ BX, $16
|
|
||||||
JLT callMemmove
|
|
||||||
|
|
||||||
// !!! Implement the copy from src to dst as a 16-byte load and store.
|
|
||||||
// (Decode's documentation says that dst and src must not overlap.)
|
|
||||||
//
|
|
||||||
// This always copies 16 bytes, instead of only length bytes, but that's
|
|
||||||
// OK. If the input is a valid Snappy encoding then subsequent iterations
|
|
||||||
// will fix up the overrun. Otherwise, Decode returns a nil []byte (and a
|
|
||||||
// non-nil error), so the overrun will be ignored.
|
|
||||||
//
|
|
||||||
// Note that on amd64, it is legal and cheap to issue unaligned 8-byte or
|
|
||||||
// 16-byte loads and stores. This technique probably wouldn't be as
|
|
||||||
// effective on architectures that are fussier about alignment.
|
|
||||||
MOVOU 0(SI), X0
|
|
||||||
MOVOU X0, 0(DI)
|
|
||||||
|
|
||||||
// d += length
|
|
||||||
// s += length
|
|
||||||
ADDQ CX, DI
|
|
||||||
ADDQ CX, SI
|
|
||||||
JMP loop
|
|
||||||
|
|
||||||
callMemmove:
|
|
||||||
// if length > len(dst)-d || length > len(src)-s { etc }
|
|
||||||
CMPQ CX, AX
|
|
||||||
JGT errCorrupt
|
|
||||||
CMPQ CX, BX
|
|
||||||
JGT errCorrupt
|
|
||||||
|
|
||||||
// copy(dst[d:], src[s:s+length])
|
|
||||||
//
|
|
||||||
// This means calling runtime·memmove(&dst[d], &src[s], length), so we push
|
|
||||||
// DI, SI and CX as arguments. Coincidentally, we also need to spill those
|
|
||||||
// three registers to the stack, to save local variables across the CALL.
|
|
||||||
MOVQ DI, 0(SP)
|
|
||||||
MOVQ SI, 8(SP)
|
|
||||||
MOVQ CX, 16(SP)
|
|
||||||
MOVQ DI, 24(SP)
|
|
||||||
MOVQ SI, 32(SP)
|
|
||||||
MOVQ CX, 40(SP)
|
|
||||||
CALL runtime·memmove(SB)
|
|
||||||
|
|
||||||
// Restore local variables: unspill registers from the stack and
|
|
||||||
// re-calculate R8-R13.
|
|
||||||
MOVQ 24(SP), DI
|
|
||||||
MOVQ 32(SP), SI
|
|
||||||
MOVQ 40(SP), CX
|
|
||||||
MOVQ dst_base+0(FP), R8
|
|
||||||
MOVQ dst_len+8(FP), R9
|
|
||||||
MOVQ R8, R10
|
|
||||||
ADDQ R9, R10
|
|
||||||
MOVQ src_base+24(FP), R11
|
|
||||||
MOVQ src_len+32(FP), R12
|
|
||||||
MOVQ R11, R13
|
|
||||||
ADDQ R12, R13
|
|
||||||
|
|
||||||
// d += length
|
|
||||||
// s += length
|
|
||||||
ADDQ CX, DI
|
|
||||||
ADDQ CX, SI
|
|
||||||
JMP loop
|
|
||||||
|
|
||||||
tagLit60Plus:
|
|
||||||
// !!! This fragment does the
|
|
||||||
//
|
|
||||||
// s += x - 58; if uint(s) > uint(len(src)) { etc }
|
|
||||||
//
|
|
||||||
// checks. In the asm version, we code it once instead of once per switch case.
|
|
||||||
ADDQ CX, SI
|
|
||||||
SUBQ $58, SI
|
|
||||||
MOVQ SI, BX
|
|
||||||
SUBQ R11, BX
|
|
||||||
CMPQ BX, R12
|
|
||||||
JA errCorrupt
|
|
||||||
|
|
||||||
// case x == 60:
|
|
||||||
CMPL CX, $61
|
|
||||||
JEQ tagLit61
|
|
||||||
JA tagLit62Plus
|
|
||||||
|
|
||||||
// x = uint32(src[s-1])
|
|
||||||
MOVBLZX -1(SI), CX
|
|
||||||
JMP doLit
|
|
||||||
|
|
||||||
tagLit61:
|
|
||||||
// case x == 61:
|
|
||||||
// x = uint32(src[s-2]) | uint32(src[s-1])<<8
|
|
||||||
MOVWLZX -2(SI), CX
|
|
||||||
JMP doLit
|
|
||||||
|
|
||||||
tagLit62Plus:
|
|
||||||
CMPL CX, $62
|
|
||||||
JA tagLit63
|
|
||||||
|
|
||||||
// case x == 62:
|
|
||||||
// x = uint32(src[s-3]) | uint32(src[s-2])<<8 | uint32(src[s-1])<<16
|
|
||||||
MOVWLZX -3(SI), CX
|
|
||||||
MOVBLZX -1(SI), BX
|
|
||||||
SHLL $16, BX
|
|
||||||
ORL BX, CX
|
|
||||||
JMP doLit
|
|
||||||
|
|
||||||
tagLit63:
|
|
||||||
// case x == 63:
|
|
||||||
// x = uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24
|
|
||||||
MOVL -4(SI), CX
|
|
||||||
JMP doLit
|
|
||||||
|
|
||||||
// The code above handles literal tags.
|
|
||||||
// ----------------------------------------
|
|
||||||
// The code below handles copy tags.
|
|
||||||
|
|
||||||
tagCopy4:
|
|
||||||
// case tagCopy4:
|
|
||||||
// s += 5
|
|
||||||
ADDQ $5, SI
|
|
||||||
|
|
||||||
// if uint(s) > uint(len(src)) { etc }
|
|
||||||
MOVQ SI, BX
|
|
||||||
SUBQ R11, BX
|
|
||||||
CMPQ BX, R12
|
|
||||||
JA errCorrupt
|
|
||||||
|
|
||||||
// length = 1 + int(src[s-5])>>2
|
|
||||||
SHRQ $2, CX
|
|
||||||
INCQ CX
|
|
||||||
|
|
||||||
// offset = int(uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24)
|
|
||||||
MOVLQZX -4(SI), DX
|
|
||||||
JMP doCopy
|
|
||||||
|
|
||||||
tagCopy2:
|
|
||||||
// case tagCopy2:
|
|
||||||
// s += 3
|
|
||||||
ADDQ $3, SI
|
|
||||||
|
|
||||||
// if uint(s) > uint(len(src)) { etc }
|
|
||||||
MOVQ SI, BX
|
|
||||||
SUBQ R11, BX
|
|
||||||
CMPQ BX, R12
|
|
||||||
JA errCorrupt
|
|
||||||
|
|
||||||
// length = 1 + int(src[s-3])>>2
|
|
||||||
SHRQ $2, CX
|
|
||||||
INCQ CX
|
|
||||||
|
|
||||||
// offset = int(uint32(src[s-2]) | uint32(src[s-1])<<8)
|
|
||||||
MOVWQZX -2(SI), DX
|
|
||||||
JMP doCopy
|
|
||||||
|
|
||||||
tagCopy:
|
|
||||||
// We have a copy tag. We assume that:
|
|
||||||
// - BX == src[s] & 0x03
|
|
||||||
// - CX == src[s]
|
|
||||||
CMPQ BX, $2
|
|
||||||
JEQ tagCopy2
|
|
||||||
JA tagCopy4
|
|
||||||
|
|
||||||
// case tagCopy1:
|
|
||||||
// s += 2
|
|
||||||
ADDQ $2, SI
|
|
||||||
|
|
||||||
// if uint(s) > uint(len(src)) { etc }
|
|
||||||
MOVQ SI, BX
|
|
||||||
SUBQ R11, BX
|
|
||||||
CMPQ BX, R12
|
|
||||||
JA errCorrupt
|
|
||||||
|
|
||||||
// offset = int(uint32(src[s-2])&0xe0<<3 | uint32(src[s-1]))
|
|
||||||
MOVQ CX, DX
|
|
||||||
ANDQ $0xe0, DX
|
|
||||||
SHLQ $3, DX
|
|
||||||
MOVBQZX -1(SI), BX
|
|
||||||
ORQ BX, DX
|
|
||||||
|
|
||||||
// length = 4 + int(src[s-2])>>2&0x7
|
|
||||||
SHRQ $2, CX
|
|
||||||
ANDQ $7, CX
|
|
||||||
ADDQ $4, CX
|
|
||||||
|
|
||||||
doCopy:
|
|
||||||
// This is the end of the outer "switch", when we have a copy tag.
|
|
||||||
//
|
|
||||||
// We assume that:
|
|
||||||
// - CX == length && CX > 0
|
|
||||||
// - DX == offset
|
|
||||||
|
|
||||||
// if offset <= 0 { etc }
|
|
||||||
CMPQ DX, $0
|
|
||||||
JLE errCorrupt
|
|
||||||
|
|
||||||
// if d < offset { etc }
|
|
||||||
MOVQ DI, BX
|
|
||||||
SUBQ R8, BX
|
|
||||||
CMPQ BX, DX
|
|
||||||
JLT errCorrupt
|
|
||||||
|
|
||||||
// if length > len(dst)-d { etc }
|
|
||||||
MOVQ R10, BX
|
|
||||||
SUBQ DI, BX
|
|
||||||
CMPQ CX, BX
|
|
||||||
JGT errCorrupt
|
|
||||||
|
|
||||||
// forwardCopy(dst[d:d+length], dst[d-offset:]); d += length
|
|
||||||
//
|
|
||||||
// Set:
|
|
||||||
// - R14 = len(dst)-d
|
|
||||||
// - R15 = &dst[d-offset]
|
|
||||||
MOVQ R10, R14
|
|
||||||
SUBQ DI, R14
|
|
||||||
MOVQ DI, R15
|
|
||||||
SUBQ DX, R15
|
|
||||||
|
|
||||||
// !!! Try a faster technique for short (16 or fewer bytes) forward copies.
|
|
||||||
//
|
|
||||||
// First, try using two 8-byte load/stores, similar to the doLit technique
|
|
||||||
// above. Even if dst[d:d+length] and dst[d-offset:] can overlap, this is
|
|
||||||
// still OK if offset >= 8. Note that this has to be two 8-byte load/stores
|
|
||||||
// and not one 16-byte load/store, and the first store has to be before the
|
|
||||||
// second load, due to the overlap if offset is in the range [8, 16).
|
|
||||||
//
|
|
||||||
// if length > 16 || offset < 8 || len(dst)-d < 16 {
|
|
||||||
// goto slowForwardCopy
|
|
||||||
// }
|
|
||||||
// copy 16 bytes
|
|
||||||
// d += length
|
|
||||||
CMPQ CX, $16
|
|
||||||
JGT slowForwardCopy
|
|
||||||
CMPQ DX, $8
|
|
||||||
JLT slowForwardCopy
|
|
||||||
CMPQ R14, $16
|
|
||||||
JLT slowForwardCopy
|
|
||||||
MOVQ 0(R15), AX
|
|
||||||
MOVQ AX, 0(DI)
|
|
||||||
MOVQ 8(R15), BX
|
|
||||||
MOVQ BX, 8(DI)
|
|
||||||
ADDQ CX, DI
|
|
||||||
JMP loop
|
|
||||||
|
|
||||||
slowForwardCopy:
|
|
||||||
// !!! If the forward copy is longer than 16 bytes, or if offset < 8, we
|
|
||||||
// can still try 8-byte load stores, provided we can overrun up to 10 extra
|
|
||||||
// bytes. As above, the overrun will be fixed up by subsequent iterations
|
|
||||||
// of the outermost loop.
|
|
||||||
//
|
|
||||||
// The C++ snappy code calls this technique IncrementalCopyFastPath. Its
|
|
||||||
// commentary says:
|
|
||||||
//
|
|
||||||
// ----
|
|
||||||
//
|
|
||||||
// The main part of this loop is a simple copy of eight bytes at a time
|
|
||||||
// until we've copied (at least) the requested amount of bytes. However,
|
|
||||||
// if d and d-offset are less than eight bytes apart (indicating a
|
|
||||||
// repeating pattern of length < 8), we first need to expand the pattern in
|
|
||||||
// order to get the correct results. For instance, if the buffer looks like
|
|
||||||
// this, with the eight-byte <d-offset> and <d> patterns marked as
|
|
||||||
// intervals:
|
|
||||||
//
|
|
||||||
// abxxxxxxxxxxxx
|
|
||||||
// [------] d-offset
|
|
||||||
// [------] d
|
|
||||||
//
|
|
||||||
// a single eight-byte copy from <d-offset> to <d> will repeat the pattern
|
|
||||||
// once, after which we can move <d> two bytes without moving <d-offset>:
|
|
||||||
//
|
|
||||||
// ababxxxxxxxxxx
|
|
||||||
// [------] d-offset
|
|
||||||
// [------] d
|
|
||||||
//
|
|
||||||
// and repeat the exercise until the two no longer overlap.
|
|
||||||
//
|
|
||||||
// This allows us to do very well in the special case of one single byte
|
|
||||||
// repeated many times, without taking a big hit for more general cases.
|
|
||||||
//
|
|
||||||
// The worst case of extra writing past the end of the match occurs when
|
|
||||||
// offset == 1 and length == 1; the last copy will read from byte positions
|
|
||||||
// [0..7] and write to [4..11], whereas it was only supposed to write to
|
|
||||||
// position 1. Thus, ten excess bytes.
|
|
||||||
//
|
|
||||||
// ----
|
|
||||||
//
|
|
||||||
// That "10 byte overrun" worst case is confirmed by Go's
|
|
||||||
// TestSlowForwardCopyOverrun, which also tests the fixUpSlowForwardCopy
|
|
||||||
// and finishSlowForwardCopy algorithm.
|
|
||||||
//
|
|
||||||
// if length > len(dst)-d-10 {
|
|
||||||
// goto verySlowForwardCopy
|
|
||||||
// }
|
|
||||||
SUBQ $10, R14
|
|
||||||
CMPQ CX, R14
|
|
||||||
JGT verySlowForwardCopy
|
|
||||||
|
|
||||||
makeOffsetAtLeast8:
|
|
||||||
// !!! As above, expand the pattern so that offset >= 8 and we can use
|
|
||||||
// 8-byte load/stores.
|
|
||||||
//
|
|
||||||
// for offset < 8 {
|
|
||||||
// copy 8 bytes from dst[d-offset:] to dst[d:]
|
|
||||||
// length -= offset
|
|
||||||
// d += offset
|
|
||||||
// offset += offset
|
|
||||||
// // The two previous lines together means that d-offset, and therefore
|
|
||||||
// // R15, is unchanged.
|
|
||||||
// }
|
|
||||||
CMPQ DX, $8
|
|
||||||
JGE fixUpSlowForwardCopy
|
|
||||||
MOVQ (R15), BX
|
|
||||||
MOVQ BX, (DI)
|
|
||||||
SUBQ DX, CX
|
|
||||||
ADDQ DX, DI
|
|
||||||
ADDQ DX, DX
|
|
||||||
JMP makeOffsetAtLeast8
|
|
||||||
|
|
||||||
fixUpSlowForwardCopy:
|
|
||||||
// !!! Add length (which might be negative now) to d (implied by DI being
|
|
||||||
// &dst[d]) so that d ends up at the right place when we jump back to the
|
|
||||||
// top of the loop. Before we do that, though, we save DI to AX so that, if
|
|
||||||
// length is positive, copying the remaining length bytes will write to the
|
|
||||||
// right place.
|
|
||||||
MOVQ DI, AX
|
|
||||||
ADDQ CX, DI
|
|
||||||
|
|
||||||
finishSlowForwardCopy:
|
|
||||||
// !!! Repeat 8-byte load/stores until length <= 0. Ending with a negative
|
|
||||||
// length means that we overrun, but as above, that will be fixed up by
|
|
||||||
// subsequent iterations of the outermost loop.
|
|
||||||
CMPQ CX, $0
|
|
||||||
JLE loop
|
|
||||||
MOVQ (R15), BX
|
|
||||||
MOVQ BX, (AX)
|
|
||||||
ADDQ $8, R15
|
|
||||||
ADDQ $8, AX
|
|
||||||
SUBQ $8, CX
|
|
||||||
JMP finishSlowForwardCopy
|
|
||||||
|
|
||||||
verySlowForwardCopy:
|
|
||||||
// verySlowForwardCopy is a simple implementation of forward copy. In C
|
|
||||||
// parlance, this is a do/while loop instead of a while loop, since we know
|
|
||||||
// that length > 0. In Go syntax:
|
|
||||||
//
|
|
||||||
// for {
|
|
||||||
// dst[d] = dst[d - offset]
|
|
||||||
// d++
|
|
||||||
// length--
|
|
||||||
// if length == 0 {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
MOVB (R15), BX
|
|
||||||
MOVB BX, (DI)
|
|
||||||
INCQ R15
|
|
||||||
INCQ DI
|
|
||||||
DECQ CX
|
|
||||||
JNZ verySlowForwardCopy
|
|
||||||
JMP loop
|
|
||||||
|
|
||||||
// The code above handles copy tags.
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
end:
|
|
||||||
// This is the end of the "for s < len(src)".
|
|
||||||
//
|
|
||||||
// if d != len(dst) { etc }
|
|
||||||
CMPQ DI, R10
|
|
||||||
JNE errCorrupt
|
|
||||||
|
|
||||||
// return 0
|
|
||||||
MOVQ $0, ret+48(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
errCorrupt:
|
|
||||||
// return decodeErrCodeCorrupt
|
|
||||||
MOVQ $1, ret+48(FP)
|
|
||||||
RET
|
|
@ -1,494 +0,0 @@
|
|||||||
// Copyright 2020 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !appengine
|
|
||||||
// +build gc
|
|
||||||
// +build !noasm
|
|
||||||
|
|
||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// The asm code generally follows the pure Go code in decode_other.go, except
|
|
||||||
// where marked with a "!!!".
|
|
||||||
|
|
||||||
// func decode(dst, src []byte) int
|
|
||||||
//
|
|
||||||
// All local variables fit into registers. The non-zero stack size is only to
|
|
||||||
// spill registers and push args when issuing a CALL. The register allocation:
|
|
||||||
// - R2 scratch
|
|
||||||
// - R3 scratch
|
|
||||||
// - R4 length or x
|
|
||||||
// - R5 offset
|
|
||||||
// - R6 &src[s]
|
|
||||||
// - R7 &dst[d]
|
|
||||||
// + R8 dst_base
|
|
||||||
// + R9 dst_len
|
|
||||||
// + R10 dst_base + dst_len
|
|
||||||
// + R11 src_base
|
|
||||||
// + R12 src_len
|
|
||||||
// + R13 src_base + src_len
|
|
||||||
// - R14 used by doCopy
|
|
||||||
// - R15 used by doCopy
|
|
||||||
//
|
|
||||||
// The registers R8-R13 (marked with a "+") are set at the start of the
|
|
||||||
// function, and after a CALL returns, and are not otherwise modified.
|
|
||||||
//
|
|
||||||
// The d variable is implicitly R7 - R8, and len(dst)-d is R10 - R7.
|
|
||||||
// The s variable is implicitly R6 - R11, and len(src)-s is R13 - R6.
|
|
||||||
TEXT ·decode(SB), NOSPLIT, $56-56
|
|
||||||
// Initialize R6, R7 and R8-R13.
|
|
||||||
MOVD dst_base+0(FP), R8
|
|
||||||
MOVD dst_len+8(FP), R9
|
|
||||||
MOVD R8, R7
|
|
||||||
MOVD R8, R10
|
|
||||||
ADD R9, R10, R10
|
|
||||||
MOVD src_base+24(FP), R11
|
|
||||||
MOVD src_len+32(FP), R12
|
|
||||||
MOVD R11, R6
|
|
||||||
MOVD R11, R13
|
|
||||||
ADD R12, R13, R13
|
|
||||||
|
|
||||||
loop:
|
|
||||||
// for s < len(src)
|
|
||||||
CMP R13, R6
|
|
||||||
BEQ end
|
|
||||||
|
|
||||||
// R4 = uint32(src[s])
|
|
||||||
//
|
|
||||||
// switch src[s] & 0x03
|
|
||||||
MOVBU (R6), R4
|
|
||||||
MOVW R4, R3
|
|
||||||
ANDW $3, R3
|
|
||||||
MOVW $1, R1
|
|
||||||
CMPW R1, R3
|
|
||||||
BGE tagCopy
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// The code below handles literal tags.
|
|
||||||
|
|
||||||
// case tagLiteral:
|
|
||||||
// x := uint32(src[s] >> 2)
|
|
||||||
// switch
|
|
||||||
MOVW $60, R1
|
|
||||||
LSRW $2, R4, R4
|
|
||||||
CMPW R4, R1
|
|
||||||
BLS tagLit60Plus
|
|
||||||
|
|
||||||
// case x < 60:
|
|
||||||
// s++
|
|
||||||
ADD $1, R6, R6
|
|
||||||
|
|
||||||
doLit:
|
|
||||||
// This is the end of the inner "switch", when we have a literal tag.
|
|
||||||
//
|
|
||||||
// We assume that R4 == x and x fits in a uint32, where x is the variable
|
|
||||||
// used in the pure Go decode_other.go code.
|
|
||||||
|
|
||||||
// length = int(x) + 1
|
|
||||||
//
|
|
||||||
// Unlike the pure Go code, we don't need to check if length <= 0 because
|
|
||||||
// R4 can hold 64 bits, so the increment cannot overflow.
|
|
||||||
ADD $1, R4, R4
|
|
||||||
|
|
||||||
// Prepare to check if copying length bytes will run past the end of dst or
|
|
||||||
// src.
|
|
||||||
//
|
|
||||||
// R2 = len(dst) - d
|
|
||||||
// R3 = len(src) - s
|
|
||||||
MOVD R10, R2
|
|
||||||
SUB R7, R2, R2
|
|
||||||
MOVD R13, R3
|
|
||||||
SUB R6, R3, R3
|
|
||||||
|
|
||||||
// !!! Try a faster technique for short (16 or fewer bytes) copies.
|
|
||||||
//
|
|
||||||
// if length > 16 || len(dst)-d < 16 || len(src)-s < 16 {
|
|
||||||
// goto callMemmove // Fall back on calling runtime·memmove.
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// The C++ snappy code calls this TryFastAppend. It also checks len(src)-s
|
|
||||||
// against 21 instead of 16, because it cannot assume that all of its input
|
|
||||||
// is contiguous in memory and so it needs to leave enough source bytes to
|
|
||||||
// read the next tag without refilling buffers, but Go's Decode assumes
|
|
||||||
// contiguousness (the src argument is a []byte).
|
|
||||||
CMP $16, R4
|
|
||||||
BGT callMemmove
|
|
||||||
CMP $16, R2
|
|
||||||
BLT callMemmove
|
|
||||||
CMP $16, R3
|
|
||||||
BLT callMemmove
|
|
||||||
|
|
||||||
// !!! Implement the copy from src to dst as a 16-byte load and store.
|
|
||||||
// (Decode's documentation says that dst and src must not overlap.)
|
|
||||||
//
|
|
||||||
// This always copies 16 bytes, instead of only length bytes, but that's
|
|
||||||
// OK. If the input is a valid Snappy encoding then subsequent iterations
|
|
||||||
// will fix up the overrun. Otherwise, Decode returns a nil []byte (and a
|
|
||||||
// non-nil error), so the overrun will be ignored.
|
|
||||||
//
|
|
||||||
// Note that on arm64, it is legal and cheap to issue unaligned 8-byte or
|
|
||||||
// 16-byte loads and stores. This technique probably wouldn't be as
|
|
||||||
// effective on architectures that are fussier about alignment.
|
|
||||||
LDP 0(R6), (R14, R15)
|
|
||||||
STP (R14, R15), 0(R7)
|
|
||||||
|
|
||||||
// d += length
|
|
||||||
// s += length
|
|
||||||
ADD R4, R7, R7
|
|
||||||
ADD R4, R6, R6
|
|
||||||
B loop
|
|
||||||
|
|
||||||
callMemmove:
|
|
||||||
// if length > len(dst)-d || length > len(src)-s { etc }
|
|
||||||
CMP R2, R4
|
|
||||||
BGT errCorrupt
|
|
||||||
CMP R3, R4
|
|
||||||
BGT errCorrupt
|
|
||||||
|
|
||||||
// copy(dst[d:], src[s:s+length])
|
|
||||||
//
|
|
||||||
// This means calling runtime·memmove(&dst[d], &src[s], length), so we push
|
|
||||||
// R7, R6 and R4 as arguments. Coincidentally, we also need to spill those
|
|
||||||
// three registers to the stack, to save local variables across the CALL.
|
|
||||||
MOVD R7, 8(RSP)
|
|
||||||
MOVD R6, 16(RSP)
|
|
||||||
MOVD R4, 24(RSP)
|
|
||||||
MOVD R7, 32(RSP)
|
|
||||||
MOVD R6, 40(RSP)
|
|
||||||
MOVD R4, 48(RSP)
|
|
||||||
CALL runtime·memmove(SB)
|
|
||||||
|
|
||||||
// Restore local variables: unspill registers from the stack and
|
|
||||||
// re-calculate R8-R13.
|
|
||||||
MOVD 32(RSP), R7
|
|
||||||
MOVD 40(RSP), R6
|
|
||||||
MOVD 48(RSP), R4
|
|
||||||
MOVD dst_base+0(FP), R8
|
|
||||||
MOVD dst_len+8(FP), R9
|
|
||||||
MOVD R8, R10
|
|
||||||
ADD R9, R10, R10
|
|
||||||
MOVD src_base+24(FP), R11
|
|
||||||
MOVD src_len+32(FP), R12
|
|
||||||
MOVD R11, R13
|
|
||||||
ADD R12, R13, R13
|
|
||||||
|
|
||||||
// d += length
|
|
||||||
// s += length
|
|
||||||
ADD R4, R7, R7
|
|
||||||
ADD R4, R6, R6
|
|
||||||
B loop
|
|
||||||
|
|
||||||
tagLit60Plus:
|
|
||||||
// !!! This fragment does the
|
|
||||||
//
|
|
||||||
// s += x - 58; if uint(s) > uint(len(src)) { etc }
|
|
||||||
//
|
|
||||||
// checks. In the asm version, we code it once instead of once per switch case.
|
|
||||||
ADD R4, R6, R6
|
|
||||||
SUB $58, R6, R6
|
|
||||||
MOVD R6, R3
|
|
||||||
SUB R11, R3, R3
|
|
||||||
CMP R12, R3
|
|
||||||
BGT errCorrupt
|
|
||||||
|
|
||||||
// case x == 60:
|
|
||||||
MOVW $61, R1
|
|
||||||
CMPW R1, R4
|
|
||||||
BEQ tagLit61
|
|
||||||
BGT tagLit62Plus
|
|
||||||
|
|
||||||
// x = uint32(src[s-1])
|
|
||||||
MOVBU -1(R6), R4
|
|
||||||
B doLit
|
|
||||||
|
|
||||||
tagLit61:
|
|
||||||
// case x == 61:
|
|
||||||
// x = uint32(src[s-2]) | uint32(src[s-1])<<8
|
|
||||||
MOVHU -2(R6), R4
|
|
||||||
B doLit
|
|
||||||
|
|
||||||
tagLit62Plus:
|
|
||||||
CMPW $62, R4
|
|
||||||
BHI tagLit63
|
|
||||||
|
|
||||||
// case x == 62:
|
|
||||||
// x = uint32(src[s-3]) | uint32(src[s-2])<<8 | uint32(src[s-1])<<16
|
|
||||||
MOVHU -3(R6), R4
|
|
||||||
MOVBU -1(R6), R3
|
|
||||||
ORR R3<<16, R4
|
|
||||||
B doLit
|
|
||||||
|
|
||||||
tagLit63:
|
|
||||||
// case x == 63:
|
|
||||||
// x = uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24
|
|
||||||
MOVWU -4(R6), R4
|
|
||||||
B doLit
|
|
||||||
|
|
||||||
// The code above handles literal tags.
|
|
||||||
// ----------------------------------------
|
|
||||||
// The code below handles copy tags.
|
|
||||||
|
|
||||||
tagCopy4:
|
|
||||||
// case tagCopy4:
|
|
||||||
// s += 5
|
|
||||||
ADD $5, R6, R6
|
|
||||||
|
|
||||||
// if uint(s) > uint(len(src)) { etc }
|
|
||||||
MOVD R6, R3
|
|
||||||
SUB R11, R3, R3
|
|
||||||
CMP R12, R3
|
|
||||||
BGT errCorrupt
|
|
||||||
|
|
||||||
// length = 1 + int(src[s-5])>>2
|
|
||||||
MOVD $1, R1
|
|
||||||
ADD R4>>2, R1, R4
|
|
||||||
|
|
||||||
// offset = int(uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24)
|
|
||||||
MOVWU -4(R6), R5
|
|
||||||
B doCopy
|
|
||||||
|
|
||||||
tagCopy2:
|
|
||||||
// case tagCopy2:
|
|
||||||
// s += 3
|
|
||||||
ADD $3, R6, R6
|
|
||||||
|
|
||||||
// if uint(s) > uint(len(src)) { etc }
|
|
||||||
MOVD R6, R3
|
|
||||||
SUB R11, R3, R3
|
|
||||||
CMP R12, R3
|
|
||||||
BGT errCorrupt
|
|
||||||
|
|
||||||
// length = 1 + int(src[s-3])>>2
|
|
||||||
MOVD $1, R1
|
|
||||||
ADD R4>>2, R1, R4
|
|
||||||
|
|
||||||
// offset = int(uint32(src[s-2]) | uint32(src[s-1])<<8)
|
|
||||||
MOVHU -2(R6), R5
|
|
||||||
B doCopy
|
|
||||||
|
|
||||||
tagCopy:
|
|
||||||
// We have a copy tag. We assume that:
|
|
||||||
// - R3 == src[s] & 0x03
|
|
||||||
// - R4 == src[s]
|
|
||||||
CMP $2, R3
|
|
||||||
BEQ tagCopy2
|
|
||||||
BGT tagCopy4
|
|
||||||
|
|
||||||
// case tagCopy1:
|
|
||||||
// s += 2
|
|
||||||
ADD $2, R6, R6
|
|
||||||
|
|
||||||
// if uint(s) > uint(len(src)) { etc }
|
|
||||||
MOVD R6, R3
|
|
||||||
SUB R11, R3, R3
|
|
||||||
CMP R12, R3
|
|
||||||
BGT errCorrupt
|
|
||||||
|
|
||||||
// offset = int(uint32(src[s-2])&0xe0<<3 | uint32(src[s-1]))
|
|
||||||
MOVD R4, R5
|
|
||||||
AND $0xe0, R5
|
|
||||||
MOVBU -1(R6), R3
|
|
||||||
ORR R5<<3, R3, R5
|
|
||||||
|
|
||||||
// length = 4 + int(src[s-2])>>2&0x7
|
|
||||||
MOVD $7, R1
|
|
||||||
AND R4>>2, R1, R4
|
|
||||||
ADD $4, R4, R4
|
|
||||||
|
|
||||||
doCopy:
|
|
||||||
// This is the end of the outer "switch", when we have a copy tag.
|
|
||||||
//
|
|
||||||
// We assume that:
|
|
||||||
// - R4 == length && R4 > 0
|
|
||||||
// - R5 == offset
|
|
||||||
|
|
||||||
// if offset <= 0 { etc }
|
|
||||||
MOVD $0, R1
|
|
||||||
CMP R1, R5
|
|
||||||
BLE errCorrupt
|
|
||||||
|
|
||||||
// if d < offset { etc }
|
|
||||||
MOVD R7, R3
|
|
||||||
SUB R8, R3, R3
|
|
||||||
CMP R5, R3
|
|
||||||
BLT errCorrupt
|
|
||||||
|
|
||||||
// if length > len(dst)-d { etc }
|
|
||||||
MOVD R10, R3
|
|
||||||
SUB R7, R3, R3
|
|
||||||
CMP R3, R4
|
|
||||||
BGT errCorrupt
|
|
||||||
|
|
||||||
// forwardCopy(dst[d:d+length], dst[d-offset:]); d += length
|
|
||||||
//
|
|
||||||
// Set:
|
|
||||||
// - R14 = len(dst)-d
|
|
||||||
// - R15 = &dst[d-offset]
|
|
||||||
MOVD R10, R14
|
|
||||||
SUB R7, R14, R14
|
|
||||||
MOVD R7, R15
|
|
||||||
SUB R5, R15, R15
|
|
||||||
|
|
||||||
// !!! Try a faster technique for short (16 or fewer bytes) forward copies.
|
|
||||||
//
|
|
||||||
// First, try using two 8-byte load/stores, similar to the doLit technique
|
|
||||||
// above. Even if dst[d:d+length] and dst[d-offset:] can overlap, this is
|
|
||||||
// still OK if offset >= 8. Note that this has to be two 8-byte load/stores
|
|
||||||
// and not one 16-byte load/store, and the first store has to be before the
|
|
||||||
// second load, due to the overlap if offset is in the range [8, 16).
|
|
||||||
//
|
|
||||||
// if length > 16 || offset < 8 || len(dst)-d < 16 {
|
|
||||||
// goto slowForwardCopy
|
|
||||||
// }
|
|
||||||
// copy 16 bytes
|
|
||||||
// d += length
|
|
||||||
CMP $16, R4
|
|
||||||
BGT slowForwardCopy
|
|
||||||
CMP $8, R5
|
|
||||||
BLT slowForwardCopy
|
|
||||||
CMP $16, R14
|
|
||||||
BLT slowForwardCopy
|
|
||||||
MOVD 0(R15), R2
|
|
||||||
MOVD R2, 0(R7)
|
|
||||||
MOVD 8(R15), R3
|
|
||||||
MOVD R3, 8(R7)
|
|
||||||
ADD R4, R7, R7
|
|
||||||
B loop
|
|
||||||
|
|
||||||
slowForwardCopy:
|
|
||||||
// !!! If the forward copy is longer than 16 bytes, or if offset < 8, we
|
|
||||||
// can still try 8-byte load stores, provided we can overrun up to 10 extra
|
|
||||||
// bytes. As above, the overrun will be fixed up by subsequent iterations
|
|
||||||
// of the outermost loop.
|
|
||||||
//
|
|
||||||
// The C++ snappy code calls this technique IncrementalCopyFastPath. Its
|
|
||||||
// commentary says:
|
|
||||||
//
|
|
||||||
// ----
|
|
||||||
//
|
|
||||||
// The main part of this loop is a simple copy of eight bytes at a time
|
|
||||||
// until we've copied (at least) the requested amount of bytes. However,
|
|
||||||
// if d and d-offset are less than eight bytes apart (indicating a
|
|
||||||
// repeating pattern of length < 8), we first need to expand the pattern in
|
|
||||||
// order to get the correct results. For instance, if the buffer looks like
|
|
||||||
// this, with the eight-byte <d-offset> and <d> patterns marked as
|
|
||||||
// intervals:
|
|
||||||
//
|
|
||||||
// abxxxxxxxxxxxx
|
|
||||||
// [------] d-offset
|
|
||||||
// [------] d
|
|
||||||
//
|
|
||||||
// a single eight-byte copy from <d-offset> to <d> will repeat the pattern
|
|
||||||
// once, after which we can move <d> two bytes without moving <d-offset>:
|
|
||||||
//
|
|
||||||
// ababxxxxxxxxxx
|
|
||||||
// [------] d-offset
|
|
||||||
// [------] d
|
|
||||||
//
|
|
||||||
// and repeat the exercise until the two no longer overlap.
|
|
||||||
//
|
|
||||||
// This allows us to do very well in the special case of one single byte
|
|
||||||
// repeated many times, without taking a big hit for more general cases.
|
|
||||||
//
|
|
||||||
// The worst case of extra writing past the end of the match occurs when
|
|
||||||
// offset == 1 and length == 1; the last copy will read from byte positions
|
|
||||||
// [0..7] and write to [4..11], whereas it was only supposed to write to
|
|
||||||
// position 1. Thus, ten excess bytes.
|
|
||||||
//
|
|
||||||
// ----
|
|
||||||
//
|
|
||||||
// That "10 byte overrun" worst case is confirmed by Go's
|
|
||||||
// TestSlowForwardCopyOverrun, which also tests the fixUpSlowForwardCopy
|
|
||||||
// and finishSlowForwardCopy algorithm.
|
|
||||||
//
|
|
||||||
// if length > len(dst)-d-10 {
|
|
||||||
// goto verySlowForwardCopy
|
|
||||||
// }
|
|
||||||
SUB $10, R14, R14
|
|
||||||
CMP R14, R4
|
|
||||||
BGT verySlowForwardCopy
|
|
||||||
|
|
||||||
makeOffsetAtLeast8:
|
|
||||||
// !!! As above, expand the pattern so that offset >= 8 and we can use
|
|
||||||
// 8-byte load/stores.
|
|
||||||
//
|
|
||||||
// for offset < 8 {
|
|
||||||
// copy 8 bytes from dst[d-offset:] to dst[d:]
|
|
||||||
// length -= offset
|
|
||||||
// d += offset
|
|
||||||
// offset += offset
|
|
||||||
// // The two previous lines together means that d-offset, and therefore
|
|
||||||
// // R15, is unchanged.
|
|
||||||
// }
|
|
||||||
CMP $8, R5
|
|
||||||
BGE fixUpSlowForwardCopy
|
|
||||||
MOVD (R15), R3
|
|
||||||
MOVD R3, (R7)
|
|
||||||
SUB R5, R4, R4
|
|
||||||
ADD R5, R7, R7
|
|
||||||
ADD R5, R5, R5
|
|
||||||
B makeOffsetAtLeast8
|
|
||||||
|
|
||||||
fixUpSlowForwardCopy:
|
|
||||||
// !!! Add length (which might be negative now) to d (implied by R7 being
|
|
||||||
// &dst[d]) so that d ends up at the right place when we jump back to the
|
|
||||||
// top of the loop. Before we do that, though, we save R7 to R2 so that, if
|
|
||||||
// length is positive, copying the remaining length bytes will write to the
|
|
||||||
// right place.
|
|
||||||
MOVD R7, R2
|
|
||||||
ADD R4, R7, R7
|
|
||||||
|
|
||||||
finishSlowForwardCopy:
|
|
||||||
// !!! Repeat 8-byte load/stores until length <= 0. Ending with a negative
|
|
||||||
// length means that we overrun, but as above, that will be fixed up by
|
|
||||||
// subsequent iterations of the outermost loop.
|
|
||||||
MOVD $0, R1
|
|
||||||
CMP R1, R4
|
|
||||||
BLE loop
|
|
||||||
MOVD (R15), R3
|
|
||||||
MOVD R3, (R2)
|
|
||||||
ADD $8, R15, R15
|
|
||||||
ADD $8, R2, R2
|
|
||||||
SUB $8, R4, R4
|
|
||||||
B finishSlowForwardCopy
|
|
||||||
|
|
||||||
verySlowForwardCopy:
|
|
||||||
// verySlowForwardCopy is a simple implementation of forward copy. In C
|
|
||||||
// parlance, this is a do/while loop instead of a while loop, since we know
|
|
||||||
// that length > 0. In Go syntax:
|
|
||||||
//
|
|
||||||
// for {
|
|
||||||
// dst[d] = dst[d - offset]
|
|
||||||
// d++
|
|
||||||
// length--
|
|
||||||
// if length == 0 {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
MOVB (R15), R3
|
|
||||||
MOVB R3, (R7)
|
|
||||||
ADD $1, R15, R15
|
|
||||||
ADD $1, R7, R7
|
|
||||||
SUB $1, R4, R4
|
|
||||||
CBNZ R4, verySlowForwardCopy
|
|
||||||
B loop
|
|
||||||
|
|
||||||
// The code above handles copy tags.
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
end:
|
|
||||||
// This is the end of the "for s < len(src)".
|
|
||||||
//
|
|
||||||
// if d != len(dst) { etc }
|
|
||||||
CMP R10, R7
|
|
||||||
BNE errCorrupt
|
|
||||||
|
|
||||||
// return 0
|
|
||||||
MOVD $0, ret+48(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
errCorrupt:
|
|
||||||
// return decodeErrCodeCorrupt
|
|
||||||
MOVD $1, R2
|
|
||||||
MOVD R2, ret+48(FP)
|
|
||||||
RET
|
|
@ -1,15 +0,0 @@
|
|||||||
// Copyright 2016 The Snappy-Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !appengine
|
|
||||||
// +build gc
|
|
||||||
// +build !noasm
|
|
||||||
// +build amd64 arm64
|
|
||||||
|
|
||||||
package snappy
|
|
||||||
|
|
||||||
// decode has the same semantics as in decode_other.go.
|
|
||||||
//
|
|
||||||
//go:noescape
|
|
||||||
func decode(dst, src []byte) int
|
|
@ -1,115 +0,0 @@
|
|||||||
// Copyright 2016 The Snappy-Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !amd64,!arm64 appengine !gc noasm
|
|
||||||
|
|
||||||
package snappy
|
|
||||||
|
|
||||||
// decode writes the decoding of src to dst. It assumes that the varint-encoded
|
|
||||||
// length of the decompressed bytes has already been read, and that len(dst)
|
|
||||||
// equals that length.
|
|
||||||
//
|
|
||||||
// It returns 0 on success or a decodeErrCodeXxx error code on failure.
|
|
||||||
func decode(dst, src []byte) int {
|
|
||||||
var d, s, offset, length int
|
|
||||||
for s < len(src) {
|
|
||||||
switch src[s] & 0x03 {
|
|
||||||
case tagLiteral:
|
|
||||||
x := uint32(src[s] >> 2)
|
|
||||||
switch {
|
|
||||||
case x < 60:
|
|
||||||
s++
|
|
||||||
case x == 60:
|
|
||||||
s += 2
|
|
||||||
if uint(s) > uint(len(src)) { // The uint conversions catch overflow from the previous line.
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
x = uint32(src[s-1])
|
|
||||||
case x == 61:
|
|
||||||
s += 3
|
|
||||||
if uint(s) > uint(len(src)) { // The uint conversions catch overflow from the previous line.
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
x = uint32(src[s-2]) | uint32(src[s-1])<<8
|
|
||||||
case x == 62:
|
|
||||||
s += 4
|
|
||||||
if uint(s) > uint(len(src)) { // The uint conversions catch overflow from the previous line.
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
x = uint32(src[s-3]) | uint32(src[s-2])<<8 | uint32(src[s-1])<<16
|
|
||||||
case x == 63:
|
|
||||||
s += 5
|
|
||||||
if uint(s) > uint(len(src)) { // The uint conversions catch overflow from the previous line.
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
x = uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24
|
|
||||||
}
|
|
||||||
length = int(x) + 1
|
|
||||||
if length <= 0 {
|
|
||||||
return decodeErrCodeUnsupportedLiteralLength
|
|
||||||
}
|
|
||||||
if length > len(dst)-d || length > len(src)-s {
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
copy(dst[d:], src[s:s+length])
|
|
||||||
d += length
|
|
||||||
s += length
|
|
||||||
continue
|
|
||||||
|
|
||||||
case tagCopy1:
|
|
||||||
s += 2
|
|
||||||
if uint(s) > uint(len(src)) { // The uint conversions catch overflow from the previous line.
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
length = 4 + int(src[s-2])>>2&0x7
|
|
||||||
offset = int(uint32(src[s-2])&0xe0<<3 | uint32(src[s-1]))
|
|
||||||
|
|
||||||
case tagCopy2:
|
|
||||||
s += 3
|
|
||||||
if uint(s) > uint(len(src)) { // The uint conversions catch overflow from the previous line.
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
length = 1 + int(src[s-3])>>2
|
|
||||||
offset = int(uint32(src[s-2]) | uint32(src[s-1])<<8)
|
|
||||||
|
|
||||||
case tagCopy4:
|
|
||||||
s += 5
|
|
||||||
if uint(s) > uint(len(src)) { // The uint conversions catch overflow from the previous line.
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
length = 1 + int(src[s-5])>>2
|
|
||||||
offset = int(uint32(src[s-4]) | uint32(src[s-3])<<8 | uint32(src[s-2])<<16 | uint32(src[s-1])<<24)
|
|
||||||
}
|
|
||||||
|
|
||||||
if offset <= 0 || d < offset || length > len(dst)-d {
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
// Copy from an earlier sub-slice of dst to a later sub-slice.
|
|
||||||
// If no overlap, use the built-in copy:
|
|
||||||
if offset >= length {
|
|
||||||
copy(dst[d:d+length], dst[d-offset:])
|
|
||||||
d += length
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlike the built-in copy function, this byte-by-byte copy always runs
|
|
||||||
// forwards, even if the slices overlap. Conceptually, this is:
|
|
||||||
//
|
|
||||||
// d += forwardCopy(dst[d:d+length], dst[d-offset:])
|
|
||||||
//
|
|
||||||
// We align the slices into a and b and show the compiler they are the same size.
|
|
||||||
// This allows the loop to run without bounds checks.
|
|
||||||
a := dst[d : d+length]
|
|
||||||
b := dst[d-offset:]
|
|
||||||
b = b[:len(a)]
|
|
||||||
for i := range a {
|
|
||||||
a[i] = b[i]
|
|
||||||
}
|
|
||||||
d += length
|
|
||||||
}
|
|
||||||
if d != len(dst) {
|
|
||||||
return decodeErrCodeCorrupt
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
@ -1,289 +0,0 @@
|
|||||||
// Copyright 2011 The Snappy-Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package snappy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/binary"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Encode returns the encoded form of src. The returned slice may be a sub-
|
|
||||||
// slice of dst if dst was large enough to hold the entire encoded block.
|
|
||||||
// Otherwise, a newly allocated slice will be returned.
|
|
||||||
//
|
|
||||||
// The dst and src must not overlap. It is valid to pass a nil dst.
|
|
||||||
//
|
|
||||||
// Encode handles the Snappy block format, not the Snappy stream format.
|
|
||||||
func Encode(dst, src []byte) []byte {
|
|
||||||
if n := MaxEncodedLen(len(src)); n < 0 {
|
|
||||||
panic(ErrTooLarge)
|
|
||||||
} else if len(dst) < n {
|
|
||||||
dst = make([]byte, n)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The block starts with the varint-encoded length of the decompressed bytes.
|
|
||||||
d := binary.PutUvarint(dst, uint64(len(src)))
|
|
||||||
|
|
||||||
for len(src) > 0 {
|
|
||||||
p := src
|
|
||||||
src = nil
|
|
||||||
if len(p) > maxBlockSize {
|
|
||||||
p, src = p[:maxBlockSize], p[maxBlockSize:]
|
|
||||||
}
|
|
||||||
if len(p) < minNonLiteralBlockSize {
|
|
||||||
d += emitLiteral(dst[d:], p)
|
|
||||||
} else {
|
|
||||||
d += encodeBlock(dst[d:], p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dst[:d]
|
|
||||||
}
|
|
||||||
|
|
||||||
// inputMargin is the minimum number of extra input bytes to keep, inside
|
|
||||||
// encodeBlock's inner loop. On some architectures, this margin lets us
|
|
||||||
// implement a fast path for emitLiteral, where the copy of short (<= 16 byte)
|
|
||||||
// literals can be implemented as a single load to and store from a 16-byte
|
|
||||||
// register. That literal's actual length can be as short as 1 byte, so this
|
|
||||||
// can copy up to 15 bytes too much, but that's OK as subsequent iterations of
|
|
||||||
// the encoding loop will fix up the copy overrun, and this inputMargin ensures
|
|
||||||
// that we don't overrun the dst and src buffers.
|
|
||||||
const inputMargin = 16 - 1
|
|
||||||
|
|
||||||
// minNonLiteralBlockSize is the minimum size of the input to encodeBlock that
|
|
||||||
// could be encoded with a copy tag. This is the minimum with respect to the
|
|
||||||
// algorithm used by encodeBlock, not a minimum enforced by the file format.
|
|
||||||
//
|
|
||||||
// The encoded output must start with at least a 1 byte literal, as there are
|
|
||||||
// no previous bytes to copy. A minimal (1 byte) copy after that, generated
|
|
||||||
// from an emitCopy call in encodeBlock's main loop, would require at least
|
|
||||||
// another inputMargin bytes, for the reason above: we want any emitLiteral
|
|
||||||
// calls inside encodeBlock's main loop to use the fast path if possible, which
|
|
||||||
// requires being able to overrun by inputMargin bytes. Thus,
|
|
||||||
// minNonLiteralBlockSize equals 1 + 1 + inputMargin.
|
|
||||||
//
|
|
||||||
// The C++ code doesn't use this exact threshold, but it could, as discussed at
|
|
||||||
// https://groups.google.com/d/topic/snappy-compression/oGbhsdIJSJ8/discussion
|
|
||||||
// The difference between Go (2+inputMargin) and C++ (inputMargin) is purely an
|
|
||||||
// optimization. It should not affect the encoded form. This is tested by
|
|
||||||
// TestSameEncodingAsCppShortCopies.
|
|
||||||
const minNonLiteralBlockSize = 1 + 1 + inputMargin
|
|
||||||
|
|
||||||
// MaxEncodedLen returns the maximum length of a snappy block, given its
|
|
||||||
// uncompressed length.
|
|
||||||
//
|
|
||||||
// It will return a negative value if srcLen is too large to encode.
|
|
||||||
func MaxEncodedLen(srcLen int) int {
|
|
||||||
n := uint64(srcLen)
|
|
||||||
if n > 0xffffffff {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
// Compressed data can be defined as:
|
|
||||||
// compressed := item* literal*
|
|
||||||
// item := literal* copy
|
|
||||||
//
|
|
||||||
// The trailing literal sequence has a space blowup of at most 62/60
|
|
||||||
// since a literal of length 60 needs one tag byte + one extra byte
|
|
||||||
// for length information.
|
|
||||||
//
|
|
||||||
// Item blowup is trickier to measure. Suppose the "copy" op copies
|
|
||||||
// 4 bytes of data. Because of a special check in the encoding code,
|
|
||||||
// we produce a 4-byte copy only if the offset is < 65536. Therefore
|
|
||||||
// the copy op takes 3 bytes to encode, and this type of item leads
|
|
||||||
// to at most the 62/60 blowup for representing literals.
|
|
||||||
//
|
|
||||||
// Suppose the "copy" op copies 5 bytes of data. If the offset is big
|
|
||||||
// enough, it will take 5 bytes to encode the copy op. Therefore the
|
|
||||||
// worst case here is a one-byte literal followed by a five-byte copy.
|
|
||||||
// That is, 6 bytes of input turn into 7 bytes of "compressed" data.
|
|
||||||
//
|
|
||||||
// This last factor dominates the blowup, so the final estimate is:
|
|
||||||
n = 32 + n + n/6
|
|
||||||
if n > 0xffffffff {
|
|
||||||
return -1
|
|
||||||
}
|
|
||||||
return int(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
var errClosed = errors.New("snappy: Writer is closed")
|
|
||||||
|
|
||||||
// NewWriter returns a new Writer that compresses to w.
|
|
||||||
//
|
|
||||||
// The Writer returned does not buffer writes. There is no need to Flush or
|
|
||||||
// Close such a Writer.
|
|
||||||
//
|
|
||||||
// Deprecated: the Writer returned is not suitable for many small writes, only
|
|
||||||
// for few large writes. Use NewBufferedWriter instead, which is efficient
|
|
||||||
// regardless of the frequency and shape of the writes, and remember to Close
|
|
||||||
// that Writer when done.
|
|
||||||
func NewWriter(w io.Writer) *Writer {
|
|
||||||
return &Writer{
|
|
||||||
w: w,
|
|
||||||
obuf: make([]byte, obufLen),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewBufferedWriter returns a new Writer that compresses to w, using the
|
|
||||||
// framing format described at
|
|
||||||
// https://github.com/google/snappy/blob/master/framing_format.txt
|
|
||||||
//
|
|
||||||
// The Writer returned buffers writes. Users must call Close to guarantee all
|
|
||||||
// data has been forwarded to the underlying io.Writer. They may also call
|
|
||||||
// Flush zero or more times before calling Close.
|
|
||||||
func NewBufferedWriter(w io.Writer) *Writer {
|
|
||||||
return &Writer{
|
|
||||||
w: w,
|
|
||||||
ibuf: make([]byte, 0, maxBlockSize),
|
|
||||||
obuf: make([]byte, obufLen),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer is an io.Writer that can write Snappy-compressed bytes.
|
|
||||||
//
|
|
||||||
// Writer handles the Snappy stream format, not the Snappy block format.
|
|
||||||
type Writer struct {
|
|
||||||
w io.Writer
|
|
||||||
err error
|
|
||||||
|
|
||||||
// ibuf is a buffer for the incoming (uncompressed) bytes.
|
|
||||||
//
|
|
||||||
// Its use is optional. For backwards compatibility, Writers created by the
|
|
||||||
// NewWriter function have ibuf == nil, do not buffer incoming bytes, and
|
|
||||||
// therefore do not need to be Flush'ed or Close'd.
|
|
||||||
ibuf []byte
|
|
||||||
|
|
||||||
// obuf is a buffer for the outgoing (compressed) bytes.
|
|
||||||
obuf []byte
|
|
||||||
|
|
||||||
// wroteStreamHeader is whether we have written the stream header.
|
|
||||||
wroteStreamHeader bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset discards the writer's state and switches the Snappy writer to write to
|
|
||||||
// w. This permits reusing a Writer rather than allocating a new one.
|
|
||||||
func (w *Writer) Reset(writer io.Writer) {
|
|
||||||
w.w = writer
|
|
||||||
w.err = nil
|
|
||||||
if w.ibuf != nil {
|
|
||||||
w.ibuf = w.ibuf[:0]
|
|
||||||
}
|
|
||||||
w.wroteStreamHeader = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write satisfies the io.Writer interface.
|
|
||||||
func (w *Writer) Write(p []byte) (nRet int, errRet error) {
|
|
||||||
if w.ibuf == nil {
|
|
||||||
// Do not buffer incoming bytes. This does not perform or compress well
|
|
||||||
// if the caller of Writer.Write writes many small slices. This
|
|
||||||
// behavior is therefore deprecated, but still supported for backwards
|
|
||||||
// compatibility with code that doesn't explicitly Flush or Close.
|
|
||||||
return w.write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The remainder of this method is based on bufio.Writer.Write from the
|
|
||||||
// standard library.
|
|
||||||
|
|
||||||
for len(p) > (cap(w.ibuf)-len(w.ibuf)) && w.err == nil {
|
|
||||||
var n int
|
|
||||||
if len(w.ibuf) == 0 {
|
|
||||||
// Large write, empty buffer.
|
|
||||||
// Write directly from p to avoid copy.
|
|
||||||
n, _ = w.write(p)
|
|
||||||
} else {
|
|
||||||
n = copy(w.ibuf[len(w.ibuf):cap(w.ibuf)], p)
|
|
||||||
w.ibuf = w.ibuf[:len(w.ibuf)+n]
|
|
||||||
w.Flush()
|
|
||||||
}
|
|
||||||
nRet += n
|
|
||||||
p = p[n:]
|
|
||||||
}
|
|
||||||
if w.err != nil {
|
|
||||||
return nRet, w.err
|
|
||||||
}
|
|
||||||
n := copy(w.ibuf[len(w.ibuf):cap(w.ibuf)], p)
|
|
||||||
w.ibuf = w.ibuf[:len(w.ibuf)+n]
|
|
||||||
nRet += n
|
|
||||||
return nRet, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *Writer) write(p []byte) (nRet int, errRet error) {
|
|
||||||
if w.err != nil {
|
|
||||||
return 0, w.err
|
|
||||||
}
|
|
||||||
for len(p) > 0 {
|
|
||||||
obufStart := len(magicChunk)
|
|
||||||
if !w.wroteStreamHeader {
|
|
||||||
w.wroteStreamHeader = true
|
|
||||||
copy(w.obuf, magicChunk)
|
|
||||||
obufStart = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var uncompressed []byte
|
|
||||||
if len(p) > maxBlockSize {
|
|
||||||
uncompressed, p = p[:maxBlockSize], p[maxBlockSize:]
|
|
||||||
} else {
|
|
||||||
uncompressed, p = p, nil
|
|
||||||
}
|
|
||||||
checksum := crc(uncompressed)
|
|
||||||
|
|
||||||
// Compress the buffer, discarding the result if the improvement
|
|
||||||
// isn't at least 12.5%.
|
|
||||||
compressed := Encode(w.obuf[obufHeaderLen:], uncompressed)
|
|
||||||
chunkType := uint8(chunkTypeCompressedData)
|
|
||||||
chunkLen := 4 + len(compressed)
|
|
||||||
obufEnd := obufHeaderLen + len(compressed)
|
|
||||||
if len(compressed) >= len(uncompressed)-len(uncompressed)/8 {
|
|
||||||
chunkType = chunkTypeUncompressedData
|
|
||||||
chunkLen = 4 + len(uncompressed)
|
|
||||||
obufEnd = obufHeaderLen
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill in the per-chunk header that comes before the body.
|
|
||||||
w.obuf[len(magicChunk)+0] = chunkType
|
|
||||||
w.obuf[len(magicChunk)+1] = uint8(chunkLen >> 0)
|
|
||||||
w.obuf[len(magicChunk)+2] = uint8(chunkLen >> 8)
|
|
||||||
w.obuf[len(magicChunk)+3] = uint8(chunkLen >> 16)
|
|
||||||
w.obuf[len(magicChunk)+4] = uint8(checksum >> 0)
|
|
||||||
w.obuf[len(magicChunk)+5] = uint8(checksum >> 8)
|
|
||||||
w.obuf[len(magicChunk)+6] = uint8(checksum >> 16)
|
|
||||||
w.obuf[len(magicChunk)+7] = uint8(checksum >> 24)
|
|
||||||
|
|
||||||
if _, err := w.w.Write(w.obuf[obufStart:obufEnd]); err != nil {
|
|
||||||
w.err = err
|
|
||||||
return nRet, err
|
|
||||||
}
|
|
||||||
if chunkType == chunkTypeUncompressedData {
|
|
||||||
if _, err := w.w.Write(uncompressed); err != nil {
|
|
||||||
w.err = err
|
|
||||||
return nRet, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nRet += len(uncompressed)
|
|
||||||
}
|
|
||||||
return nRet, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush flushes the Writer to its underlying io.Writer.
|
|
||||||
func (w *Writer) Flush() error {
|
|
||||||
if w.err != nil {
|
|
||||||
return w.err
|
|
||||||
}
|
|
||||||
if len(w.ibuf) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
w.write(w.ibuf)
|
|
||||||
w.ibuf = w.ibuf[:0]
|
|
||||||
return w.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close calls Flush and then closes the Writer.
|
|
||||||
func (w *Writer) Close() error {
|
|
||||||
w.Flush()
|
|
||||||
ret := w.err
|
|
||||||
if w.err == nil {
|
|
||||||
w.err = errClosed
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
|
@ -1,730 +0,0 @@
|
|||||||
// Copyright 2016 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
// +build !appengine
|
|
||||||
// +build gc
|
|
||||||
// +build !noasm
|
|
||||||
|
|
||||||
#include "textflag.h"
|
|
||||||
|
|
||||||
// The XXX lines assemble on Go 1.4, 1.5 and 1.7, but not 1.6, due to a
|
|
||||||
// Go toolchain regression. See https://github.com/golang/go/issues/15426 and
|
|
||||||
// https://github.com/golang/snappy/issues/29
|
|
||||||
//
|
|
||||||
// As a workaround, the package was built with a known good assembler, and
|
|
||||||
// those instructions were disassembled by "objdump -d" to yield the
|
|
||||||
// 4e 0f b7 7c 5c 78 movzwq 0x78(%rsp,%r11,2),%r15
|
|
||||||
// style comments, in AT&T asm syntax. Note that rsp here is a physical
|
|
||||||
// register, not Go/asm's SP pseudo-register (see https://golang.org/doc/asm).
|
|
||||||
// The instructions were then encoded as "BYTE $0x.." sequences, which assemble
|
|
||||||
// fine on Go 1.6.
|
|
||||||
|
|
||||||
// The asm code generally follows the pure Go code in encode_other.go, except
|
|
||||||
// where marked with a "!!!".
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// func emitLiteral(dst, lit []byte) int
|
|
||||||
//
|
|
||||||
// All local variables fit into registers. The register allocation:
|
|
||||||
// - AX len(lit)
|
|
||||||
// - BX n
|
|
||||||
// - DX return value
|
|
||||||
// - DI &dst[i]
|
|
||||||
// - R10 &lit[0]
|
|
||||||
//
|
|
||||||
// The 24 bytes of stack space is to call runtime·memmove.
|
|
||||||
//
|
|
||||||
// The unusual register allocation of local variables, such as R10 for the
|
|
||||||
// source pointer, matches the allocation used at the call site in encodeBlock,
|
|
||||||
// which makes it easier to manually inline this function.
|
|
||||||
TEXT ·emitLiteral(SB), NOSPLIT, $24-56
|
|
||||||
MOVQ dst_base+0(FP), DI
|
|
||||||
MOVQ lit_base+24(FP), R10
|
|
||||||
MOVQ lit_len+32(FP), AX
|
|
||||||
MOVQ AX, DX
|
|
||||||
MOVL AX, BX
|
|
||||||
SUBL $1, BX
|
|
||||||
|
|
||||||
CMPL BX, $60
|
|
||||||
JLT oneByte
|
|
||||||
CMPL BX, $256
|
|
||||||
JLT twoBytes
|
|
||||||
|
|
||||||
threeBytes:
|
|
||||||
MOVB $0xf4, 0(DI)
|
|
||||||
MOVW BX, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
ADDQ $3, DX
|
|
||||||
JMP memmove
|
|
||||||
|
|
||||||
twoBytes:
|
|
||||||
MOVB $0xf0, 0(DI)
|
|
||||||
MOVB BX, 1(DI)
|
|
||||||
ADDQ $2, DI
|
|
||||||
ADDQ $2, DX
|
|
||||||
JMP memmove
|
|
||||||
|
|
||||||
oneByte:
|
|
||||||
SHLB $2, BX
|
|
||||||
MOVB BX, 0(DI)
|
|
||||||
ADDQ $1, DI
|
|
||||||
ADDQ $1, DX
|
|
||||||
|
|
||||||
memmove:
|
|
||||||
MOVQ DX, ret+48(FP)
|
|
||||||
|
|
||||||
// copy(dst[i:], lit)
|
|
||||||
//
|
|
||||||
// This means calling runtime·memmove(&dst[i], &lit[0], len(lit)), so we push
|
|
||||||
// DI, R10 and AX as arguments.
|
|
||||||
MOVQ DI, 0(SP)
|
|
||||||
MOVQ R10, 8(SP)
|
|
||||||
MOVQ AX, 16(SP)
|
|
||||||
CALL runtime·memmove(SB)
|
|
||||||
RET
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// func emitCopy(dst []byte, offset, length int) int
|
|
||||||
//
|
|
||||||
// All local variables fit into registers. The register allocation:
|
|
||||||
// - AX length
|
|
||||||
// - SI &dst[0]
|
|
||||||
// - DI &dst[i]
|
|
||||||
// - R11 offset
|
|
||||||
//
|
|
||||||
// The unusual register allocation of local variables, such as R11 for the
|
|
||||||
// offset, matches the allocation used at the call site in encodeBlock, which
|
|
||||||
// makes it easier to manually inline this function.
|
|
||||||
TEXT ·emitCopy(SB), NOSPLIT, $0-48
|
|
||||||
MOVQ dst_base+0(FP), DI
|
|
||||||
MOVQ DI, SI
|
|
||||||
MOVQ offset+24(FP), R11
|
|
||||||
MOVQ length+32(FP), AX
|
|
||||||
|
|
||||||
loop0:
|
|
||||||
// for length >= 68 { etc }
|
|
||||||
CMPL AX, $68
|
|
||||||
JLT step1
|
|
||||||
|
|
||||||
// Emit a length 64 copy, encoded as 3 bytes.
|
|
||||||
MOVB $0xfe, 0(DI)
|
|
||||||
MOVW R11, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
SUBL $64, AX
|
|
||||||
JMP loop0
|
|
||||||
|
|
||||||
step1:
|
|
||||||
// if length > 64 { etc }
|
|
||||||
CMPL AX, $64
|
|
||||||
JLE step2
|
|
||||||
|
|
||||||
// Emit a length 60 copy, encoded as 3 bytes.
|
|
||||||
MOVB $0xee, 0(DI)
|
|
||||||
MOVW R11, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
SUBL $60, AX
|
|
||||||
|
|
||||||
step2:
|
|
||||||
// if length >= 12 || offset >= 2048 { goto step3 }
|
|
||||||
CMPL AX, $12
|
|
||||||
JGE step3
|
|
||||||
CMPL R11, $2048
|
|
||||||
JGE step3
|
|
||||||
|
|
||||||
// Emit the remaining copy, encoded as 2 bytes.
|
|
||||||
MOVB R11, 1(DI)
|
|
||||||
SHRL $8, R11
|
|
||||||
SHLB $5, R11
|
|
||||||
SUBB $4, AX
|
|
||||||
SHLB $2, AX
|
|
||||||
ORB AX, R11
|
|
||||||
ORB $1, R11
|
|
||||||
MOVB R11, 0(DI)
|
|
||||||
ADDQ $2, DI
|
|
||||||
|
|
||||||
// Return the number of bytes written.
|
|
||||||
SUBQ SI, DI
|
|
||||||
MOVQ DI, ret+40(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
step3:
|
|
||||||
// Emit the remaining copy, encoded as 3 bytes.
|
|
||||||
SUBL $1, AX
|
|
||||||
SHLB $2, AX
|
|
||||||
ORB $2, AX
|
|
||||||
MOVB AX, 0(DI)
|
|
||||||
MOVW R11, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
|
|
||||||
// Return the number of bytes written.
|
|
||||||
SUBQ SI, DI
|
|
||||||
MOVQ DI, ret+40(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// func extendMatch(src []byte, i, j int) int
|
|
||||||
//
|
|
||||||
// All local variables fit into registers. The register allocation:
|
|
||||||
// - DX &src[0]
|
|
||||||
// - SI &src[j]
|
|
||||||
// - R13 &src[len(src) - 8]
|
|
||||||
// - R14 &src[len(src)]
|
|
||||||
// - R15 &src[i]
|
|
||||||
//
|
|
||||||
// The unusual register allocation of local variables, such as R15 for a source
|
|
||||||
// pointer, matches the allocation used at the call site in encodeBlock, which
|
|
||||||
// makes it easier to manually inline this function.
|
|
||||||
TEXT ·extendMatch(SB), NOSPLIT, $0-48
|
|
||||||
MOVQ src_base+0(FP), DX
|
|
||||||
MOVQ src_len+8(FP), R14
|
|
||||||
MOVQ i+24(FP), R15
|
|
||||||
MOVQ j+32(FP), SI
|
|
||||||
ADDQ DX, R14
|
|
||||||
ADDQ DX, R15
|
|
||||||
ADDQ DX, SI
|
|
||||||
MOVQ R14, R13
|
|
||||||
SUBQ $8, R13
|
|
||||||
|
|
||||||
cmp8:
|
|
||||||
// As long as we are 8 or more bytes before the end of src, we can load and
|
|
||||||
// compare 8 bytes at a time. If those 8 bytes are equal, repeat.
|
|
||||||
CMPQ SI, R13
|
|
||||||
JA cmp1
|
|
||||||
MOVQ (R15), AX
|
|
||||||
MOVQ (SI), BX
|
|
||||||
CMPQ AX, BX
|
|
||||||
JNE bsf
|
|
||||||
ADDQ $8, R15
|
|
||||||
ADDQ $8, SI
|
|
||||||
JMP cmp8
|
|
||||||
|
|
||||||
bsf:
|
|
||||||
// If those 8 bytes were not equal, XOR the two 8 byte values, and return
|
|
||||||
// the index of the first byte that differs. The BSF instruction finds the
|
|
||||||
// least significant 1 bit, the amd64 architecture is little-endian, and
|
|
||||||
// the shift by 3 converts a bit index to a byte index.
|
|
||||||
XORQ AX, BX
|
|
||||||
BSFQ BX, BX
|
|
||||||
SHRQ $3, BX
|
|
||||||
ADDQ BX, SI
|
|
||||||
|
|
||||||
// Convert from &src[ret] to ret.
|
|
||||||
SUBQ DX, SI
|
|
||||||
MOVQ SI, ret+40(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
cmp1:
|
|
||||||
// In src's tail, compare 1 byte at a time.
|
|
||||||
CMPQ SI, R14
|
|
||||||
JAE extendMatchEnd
|
|
||||||
MOVB (R15), AX
|
|
||||||
MOVB (SI), BX
|
|
||||||
CMPB AX, BX
|
|
||||||
JNE extendMatchEnd
|
|
||||||
ADDQ $1, R15
|
|
||||||
ADDQ $1, SI
|
|
||||||
JMP cmp1
|
|
||||||
|
|
||||||
extendMatchEnd:
|
|
||||||
// Convert from &src[ret] to ret.
|
|
||||||
SUBQ DX, SI
|
|
||||||
MOVQ SI, ret+40(FP)
|
|
||||||
RET
|
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// func encodeBlock(dst, src []byte) (d int)
|
|
||||||
//
|
|
||||||
// All local variables fit into registers, other than "var table". The register
|
|
||||||
// allocation:
|
|
||||||
// - AX . .
|
|
||||||
// - BX . .
|
|
||||||
// - CX 56 shift (note that amd64 shifts by non-immediates must use CX).
|
|
||||||
// - DX 64 &src[0], tableSize
|
|
||||||
// - SI 72 &src[s]
|
|
||||||
// - DI 80 &dst[d]
|
|
||||||
// - R9 88 sLimit
|
|
||||||
// - R10 . &src[nextEmit]
|
|
||||||
// - R11 96 prevHash, currHash, nextHash, offset
|
|
||||||
// - R12 104 &src[base], skip
|
|
||||||
// - R13 . &src[nextS], &src[len(src) - 8]
|
|
||||||
// - R14 . len(src), bytesBetweenHashLookups, &src[len(src)], x
|
|
||||||
// - R15 112 candidate
|
|
||||||
//
|
|
||||||
// The second column (56, 64, etc) is the stack offset to spill the registers
|
|
||||||
// when calling other functions. We could pack this slightly tighter, but it's
|
|
||||||
// simpler to have a dedicated spill map independent of the function called.
|
|
||||||
//
|
|
||||||
// "var table [maxTableSize]uint16" takes up 32768 bytes of stack space. An
|
|
||||||
// extra 56 bytes, to call other functions, and an extra 64 bytes, to spill
|
|
||||||
// local variables (registers) during calls gives 32768 + 56 + 64 = 32888.
|
|
||||||
TEXT ·encodeBlock(SB), 0, $32888-56
|
|
||||||
MOVQ dst_base+0(FP), DI
|
|
||||||
MOVQ src_base+24(FP), SI
|
|
||||||
MOVQ src_len+32(FP), R14
|
|
||||||
|
|
||||||
// shift, tableSize := uint32(32-8), 1<<8
|
|
||||||
MOVQ $24, CX
|
|
||||||
MOVQ $256, DX
|
|
||||||
|
|
||||||
calcShift:
|
|
||||||
// for ; tableSize < maxTableSize && tableSize < len(src); tableSize *= 2 {
|
|
||||||
// shift--
|
|
||||||
// }
|
|
||||||
CMPQ DX, $16384
|
|
||||||
JGE varTable
|
|
||||||
CMPQ DX, R14
|
|
||||||
JGE varTable
|
|
||||||
SUBQ $1, CX
|
|
||||||
SHLQ $1, DX
|
|
||||||
JMP calcShift
|
|
||||||
|
|
||||||
varTable:
|
|
||||||
// var table [maxTableSize]uint16
|
|
||||||
//
|
|
||||||
// In the asm code, unlike the Go code, we can zero-initialize only the
|
|
||||||
// first tableSize elements. Each uint16 element is 2 bytes and each MOVOU
|
|
||||||
// writes 16 bytes, so we can do only tableSize/8 writes instead of the
|
|
||||||
// 2048 writes that would zero-initialize all of table's 32768 bytes.
|
|
||||||
SHRQ $3, DX
|
|
||||||
LEAQ table-32768(SP), BX
|
|
||||||
PXOR X0, X0
|
|
||||||
|
|
||||||
memclr:
|
|
||||||
MOVOU X0, 0(BX)
|
|
||||||
ADDQ $16, BX
|
|
||||||
SUBQ $1, DX
|
|
||||||
JNZ memclr
|
|
||||||
|
|
||||||
// !!! DX = &src[0]
|
|
||||||
MOVQ SI, DX
|
|
||||||
|
|
||||||
// sLimit := len(src) - inputMargin
|
|
||||||
MOVQ R14, R9
|
|
||||||
SUBQ $15, R9
|
|
||||||
|
|
||||||
// !!! Pre-emptively spill CX, DX and R9 to the stack. Their values don't
|
|
||||||
// change for the rest of the function.
|
|
||||||
MOVQ CX, 56(SP)
|
|
||||||
MOVQ DX, 64(SP)
|
|
||||||
MOVQ R9, 88(SP)
|
|
||||||
|
|
||||||
// nextEmit := 0
|
|
||||||
MOVQ DX, R10
|
|
||||||
|
|
||||||
// s := 1
|
|
||||||
ADDQ $1, SI
|
|
||||||
|
|
||||||
// nextHash := hash(load32(src, s), shift)
|
|
||||||
MOVL 0(SI), R11
|
|
||||||
IMULL $0x1e35a7bd, R11
|
|
||||||
SHRL CX, R11
|
|
||||||
|
|
||||||
outer:
|
|
||||||
// for { etc }
|
|
||||||
|
|
||||||
// skip := 32
|
|
||||||
MOVQ $32, R12
|
|
||||||
|
|
||||||
// nextS := s
|
|
||||||
MOVQ SI, R13
|
|
||||||
|
|
||||||
// candidate := 0
|
|
||||||
MOVQ $0, R15
|
|
||||||
|
|
||||||
inner0:
|
|
||||||
// for { etc }
|
|
||||||
|
|
||||||
// s := nextS
|
|
||||||
MOVQ R13, SI
|
|
||||||
|
|
||||||
// bytesBetweenHashLookups := skip >> 5
|
|
||||||
MOVQ R12, R14
|
|
||||||
SHRQ $5, R14
|
|
||||||
|
|
||||||
// nextS = s + bytesBetweenHashLookups
|
|
||||||
ADDQ R14, R13
|
|
||||||
|
|
||||||
// skip += bytesBetweenHashLookups
|
|
||||||
ADDQ R14, R12
|
|
||||||
|
|
||||||
// if nextS > sLimit { goto emitRemainder }
|
|
||||||
MOVQ R13, AX
|
|
||||||
SUBQ DX, AX
|
|
||||||
CMPQ AX, R9
|
|
||||||
JA emitRemainder
|
|
||||||
|
|
||||||
// candidate = int(table[nextHash])
|
|
||||||
// XXX: MOVWQZX table-32768(SP)(R11*2), R15
|
|
||||||
// XXX: 4e 0f b7 7c 5c 78 movzwq 0x78(%rsp,%r11,2),%r15
|
|
||||||
BYTE $0x4e
|
|
||||||
BYTE $0x0f
|
|
||||||
BYTE $0xb7
|
|
||||||
BYTE $0x7c
|
|
||||||
BYTE $0x5c
|
|
||||||
BYTE $0x78
|
|
||||||
|
|
||||||
// table[nextHash] = uint16(s)
|
|
||||||
MOVQ SI, AX
|
|
||||||
SUBQ DX, AX
|
|
||||||
|
|
||||||
// XXX: MOVW AX, table-32768(SP)(R11*2)
|
|
||||||
// XXX: 66 42 89 44 5c 78 mov %ax,0x78(%rsp,%r11,2)
|
|
||||||
BYTE $0x66
|
|
||||||
BYTE $0x42
|
|
||||||
BYTE $0x89
|
|
||||||
BYTE $0x44
|
|
||||||
BYTE $0x5c
|
|
||||||
BYTE $0x78
|
|
||||||
|
|
||||||
// nextHash = hash(load32(src, nextS), shift)
|
|
||||||
MOVL 0(R13), R11
|
|
||||||
IMULL $0x1e35a7bd, R11
|
|
||||||
SHRL CX, R11
|
|
||||||
|
|
||||||
// if load32(src, s) != load32(src, candidate) { continue } break
|
|
||||||
MOVL 0(SI), AX
|
|
||||||
MOVL (DX)(R15*1), BX
|
|
||||||
CMPL AX, BX
|
|
||||||
JNE inner0
|
|
||||||
|
|
||||||
fourByteMatch:
|
|
||||||
// As per the encode_other.go code:
|
|
||||||
//
|
|
||||||
// A 4-byte match has been found. We'll later see etc.
|
|
||||||
|
|
||||||
// !!! Jump to a fast path for short (<= 16 byte) literals. See the comment
|
|
||||||
// on inputMargin in encode.go.
|
|
||||||
MOVQ SI, AX
|
|
||||||
SUBQ R10, AX
|
|
||||||
CMPQ AX, $16
|
|
||||||
JLE emitLiteralFastPath
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Begin inline of the emitLiteral call.
|
|
||||||
//
|
|
||||||
// d += emitLiteral(dst[d:], src[nextEmit:s])
|
|
||||||
|
|
||||||
MOVL AX, BX
|
|
||||||
SUBL $1, BX
|
|
||||||
|
|
||||||
CMPL BX, $60
|
|
||||||
JLT inlineEmitLiteralOneByte
|
|
||||||
CMPL BX, $256
|
|
||||||
JLT inlineEmitLiteralTwoBytes
|
|
||||||
|
|
||||||
inlineEmitLiteralThreeBytes:
|
|
||||||
MOVB $0xf4, 0(DI)
|
|
||||||
MOVW BX, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
JMP inlineEmitLiteralMemmove
|
|
||||||
|
|
||||||
inlineEmitLiteralTwoBytes:
|
|
||||||
MOVB $0xf0, 0(DI)
|
|
||||||
MOVB BX, 1(DI)
|
|
||||||
ADDQ $2, DI
|
|
||||||
JMP inlineEmitLiteralMemmove
|
|
||||||
|
|
||||||
inlineEmitLiteralOneByte:
|
|
||||||
SHLB $2, BX
|
|
||||||
MOVB BX, 0(DI)
|
|
||||||
ADDQ $1, DI
|
|
||||||
|
|
||||||
inlineEmitLiteralMemmove:
|
|
||||||
// Spill local variables (registers) onto the stack; call; unspill.
|
|
||||||
//
|
|
||||||
// copy(dst[i:], lit)
|
|
||||||
//
|
|
||||||
// This means calling runtime·memmove(&dst[i], &lit[0], len(lit)), so we push
|
|
||||||
// DI, R10 and AX as arguments.
|
|
||||||
MOVQ DI, 0(SP)
|
|
||||||
MOVQ R10, 8(SP)
|
|
||||||
MOVQ AX, 16(SP)
|
|
||||||
ADDQ AX, DI // Finish the "d +=" part of "d += emitLiteral(etc)".
|
|
||||||
MOVQ SI, 72(SP)
|
|
||||||
MOVQ DI, 80(SP)
|
|
||||||
MOVQ R15, 112(SP)
|
|
||||||
CALL runtime·memmove(SB)
|
|
||||||
MOVQ 56(SP), CX
|
|
||||||
MOVQ 64(SP), DX
|
|
||||||
MOVQ 72(SP), SI
|
|
||||||
MOVQ 80(SP), DI
|
|
||||||
MOVQ 88(SP), R9
|
|
||||||
MOVQ 112(SP), R15
|
|
||||||
JMP inner1
|
|
||||||
|
|
||||||
inlineEmitLiteralEnd:
|
|
||||||
// End inline of the emitLiteral call.
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
emitLiteralFastPath:
|
|
||||||
// !!! Emit the 1-byte encoding "uint8(len(lit)-1)<<2".
|
|
||||||
MOVB AX, BX
|
|
||||||
SUBB $1, BX
|
|
||||||
SHLB $2, BX
|
|
||||||
MOVB BX, (DI)
|
|
||||||
ADDQ $1, DI
|
|
||||||
|
|
||||||
// !!! Implement the copy from lit to dst as a 16-byte load and store.
|
|
||||||
// (Encode's documentation says that dst and src must not overlap.)
|
|
||||||
//
|
|
||||||
// This always copies 16 bytes, instead of only len(lit) bytes, but that's
|
|
||||||
// OK. Subsequent iterations will fix up the overrun.
|
|
||||||
//
|
|
||||||
// Note that on amd64, it is legal and cheap to issue unaligned 8-byte or
|
|
||||||
// 16-byte loads and stores. This technique probably wouldn't be as
|
|
||||||
// effective on architectures that are fussier about alignment.
|
|
||||||
MOVOU 0(R10), X0
|
|
||||||
MOVOU X0, 0(DI)
|
|
||||||
ADDQ AX, DI
|
|
||||||
|
|
||||||
inner1:
|
|
||||||
// for { etc }
|
|
||||||
|
|
||||||
// base := s
|
|
||||||
MOVQ SI, R12
|
|
||||||
|
|
||||||
// !!! offset := base - candidate
|
|
||||||
MOVQ R12, R11
|
|
||||||
SUBQ R15, R11
|
|
||||||
SUBQ DX, R11
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Begin inline of the extendMatch call.
|
|
||||||
//
|
|
||||||
// s = extendMatch(src, candidate+4, s+4)
|
|
||||||
|
|
||||||
// !!! R14 = &src[len(src)]
|
|
||||||
MOVQ src_len+32(FP), R14
|
|
||||||
ADDQ DX, R14
|
|
||||||
|
|
||||||
// !!! R13 = &src[len(src) - 8]
|
|
||||||
MOVQ R14, R13
|
|
||||||
SUBQ $8, R13
|
|
||||||
|
|
||||||
// !!! R15 = &src[candidate + 4]
|
|
||||||
ADDQ $4, R15
|
|
||||||
ADDQ DX, R15
|
|
||||||
|
|
||||||
// !!! s += 4
|
|
||||||
ADDQ $4, SI
|
|
||||||
|
|
||||||
inlineExtendMatchCmp8:
|
|
||||||
// As long as we are 8 or more bytes before the end of src, we can load and
|
|
||||||
// compare 8 bytes at a time. If those 8 bytes are equal, repeat.
|
|
||||||
CMPQ SI, R13
|
|
||||||
JA inlineExtendMatchCmp1
|
|
||||||
MOVQ (R15), AX
|
|
||||||
MOVQ (SI), BX
|
|
||||||
CMPQ AX, BX
|
|
||||||
JNE inlineExtendMatchBSF
|
|
||||||
ADDQ $8, R15
|
|
||||||
ADDQ $8, SI
|
|
||||||
JMP inlineExtendMatchCmp8
|
|
||||||
|
|
||||||
inlineExtendMatchBSF:
|
|
||||||
// If those 8 bytes were not equal, XOR the two 8 byte values, and return
|
|
||||||
// the index of the first byte that differs. The BSF instruction finds the
|
|
||||||
// least significant 1 bit, the amd64 architecture is little-endian, and
|
|
||||||
// the shift by 3 converts a bit index to a byte index.
|
|
||||||
XORQ AX, BX
|
|
||||||
BSFQ BX, BX
|
|
||||||
SHRQ $3, BX
|
|
||||||
ADDQ BX, SI
|
|
||||||
JMP inlineExtendMatchEnd
|
|
||||||
|
|
||||||
inlineExtendMatchCmp1:
|
|
||||||
// In src's tail, compare 1 byte at a time.
|
|
||||||
CMPQ SI, R14
|
|
||||||
JAE inlineExtendMatchEnd
|
|
||||||
MOVB (R15), AX
|
|
||||||
MOVB (SI), BX
|
|
||||||
CMPB AX, BX
|
|
||||||
JNE inlineExtendMatchEnd
|
|
||||||
ADDQ $1, R15
|
|
||||||
ADDQ $1, SI
|
|
||||||
JMP inlineExtendMatchCmp1
|
|
||||||
|
|
||||||
inlineExtendMatchEnd:
|
|
||||||
// End inline of the extendMatch call.
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
// ----------------------------------------
|
|
||||||
// Begin inline of the emitCopy call.
|
|
||||||
//
|
|
||||||
// d += emitCopy(dst[d:], base-candidate, s-base)
|
|
||||||
|
|
||||||
// !!! length := s - base
|
|
||||||
MOVQ SI, AX
|
|
||||||
SUBQ R12, AX
|
|
||||||
|
|
||||||
inlineEmitCopyLoop0:
|
|
||||||
// for length >= 68 { etc }
|
|
||||||
CMPL AX, $68
|
|
||||||
JLT inlineEmitCopyStep1
|
|
||||||
|
|
||||||
// Emit a length 64 copy, encoded as 3 bytes.
|
|
||||||
MOVB $0xfe, 0(DI)
|
|
||||||
MOVW R11, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
SUBL $64, AX
|
|
||||||
JMP inlineEmitCopyLoop0
|
|
||||||
|
|
||||||
inlineEmitCopyStep1:
|
|
||||||
// if length > 64 { etc }
|
|
||||||
CMPL AX, $64
|
|
||||||
JLE inlineEmitCopyStep2
|
|
||||||
|
|
||||||
// Emit a length 60 copy, encoded as 3 bytes.
|
|
||||||
MOVB $0xee, 0(DI)
|
|
||||||
MOVW R11, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
SUBL $60, AX
|
|
||||||
|
|
||||||
inlineEmitCopyStep2:
|
|
||||||
// if length >= 12 || offset >= 2048 { goto inlineEmitCopyStep3 }
|
|
||||||
CMPL AX, $12
|
|
||||||
JGE inlineEmitCopyStep3
|
|
||||||
CMPL R11, $2048
|
|
||||||
JGE inlineEmitCopyStep3
|
|
||||||
|
|
||||||
// Emit the remaining copy, encoded as 2 bytes.
|
|
||||||
MOVB R11, 1(DI)
|
|
||||||
SHRL $8, R11
|
|
||||||
SHLB $5, R11
|
|
||||||
SUBB $4, AX
|
|
||||||
SHLB $2, AX
|
|
||||||
ORB AX, R11
|
|
||||||
ORB $1, R11
|
|
||||||
MOVB R11, 0(DI)
|
|
||||||
ADDQ $2, DI
|
|
||||||
JMP inlineEmitCopyEnd
|
|
||||||
|
|
||||||
inlineEmitCopyStep3:
|
|
||||||
// Emit the remaining copy, encoded as 3 bytes.
|
|
||||||
SUBL $1, AX
|
|
||||||
SHLB $2, AX
|
|
||||||
ORB $2, AX
|
|
||||||
MOVB AX, 0(DI)
|
|
||||||
MOVW R11, 1(DI)
|
|
||||||
ADDQ $3, DI
|
|
||||||
|
|
||||||
inlineEmitCopyEnd:
|
|
||||||
// End inline of the emitCopy call.
|
|
||||||
// ----------------------------------------
|
|
||||||
|
|
||||||
// nextEmit = s
|
|
||||||
MOVQ SI, R10
|
|
||||||
|
|
||||||
// if s >= sLimit { goto emitRemainder }
|
|
||||||
MOVQ SI, AX
|
|
||||||
SUBQ DX, AX
|
|
||||||
CMPQ AX, R9
|
|
||||||
JAE emitRemainder
|
|
||||||
|
|
||||||
// As per the encode_other.go code:
|
|
||||||
//
|
|
||||||
// We could immediately etc.
|
|
||||||
|
|
||||||
// x := load64(src, s-1)
|
|
||||||
MOVQ -1(SI), R14
|
|
||||||
|
|
||||||
// prevHash := hash(uint32(x>>0), shift)
|
|
||||||
MOVL R14, R11
|
|
||||||
IMULL $0x1e35a7bd, R11
|
|
||||||
SHRL CX, R11
|
|
||||||
|
|
||||||
// table[prevHash] = uint16(s-1)
|
|
||||||
MOVQ SI, AX
|
|
||||||
SUBQ DX, AX
|
|
||||||
SUBQ $1, AX
|
|
||||||
|
|
||||||
// XXX: MOVW AX, table-32768(SP)(R11*2)
|
|
||||||
// XXX: 66 42 89 44 5c 78 mov %ax,0x78(%rsp,%r11,2)
|
|
||||||
BYTE $0x66
|
|
||||||
BYTE $0x42
|
|
||||||
BYTE $0x89
|
|
||||||
BYTE $0x44
|
|
||||||
BYTE $0x5c
|
|
||||||
BYTE $0x78
|
|
||||||
|
|
||||||
// currHash := hash(uint32(x>>8), shift)
|
|
||||||
SHRQ $8, R14
|
|
||||||
MOVL R14, R11
|
|
||||||
IMULL $0x1e35a7bd, R11
|
|
||||||
SHRL CX, R11
|
|
||||||
|
|
||||||
// candidate = int(table[currHash])
|
|
||||||
// XXX: MOVWQZX table-32768(SP)(R11*2), R15
|
|
||||||
// XXX: 4e 0f b7 7c 5c 78 movzwq 0x78(%rsp,%r11,2),%r15
|
|
||||||
BYTE $0x4e
|
|
||||||
BYTE $0x0f
|
|
||||||
BYTE $0xb7
|
|
||||||
BYTE $0x7c
|
|
||||||
BYTE $0x5c
|
|
||||||
BYTE $0x78
|
|
||||||
|
|
||||||
// table[currHash] = uint16(s)
|
|
||||||
ADDQ $1, AX
|
|
||||||
|
|
||||||
// XXX: MOVW AX, table-32768(SP)(R11*2)
|
|
||||||
// XXX: 66 42 89 44 5c 78 mov %ax,0x78(%rsp,%r11,2)
|
|
||||||
BYTE $0x66
|
|
||||||
BYTE $0x42
|
|
||||||
BYTE $0x89
|
|
||||||
BYTE $0x44
|
|
||||||
BYTE $0x5c
|
|
||||||
BYTE $0x78
|
|
||||||
|
|
||||||
// if uint32(x>>8) == load32(src, candidate) { continue }
|
|
||||||
MOVL (DX)(R15*1), BX
|
|
||||||
CMPL R14, BX
|
|
||||||
JEQ inner1
|
|
||||||
|
|
||||||
// nextHash = hash(uint32(x>>16), shift)
|
|
||||||
SHRQ $8, R14
|
|
||||||
MOVL R14, R11
|
|
||||||
IMULL $0x1e35a7bd, R11
|
|
||||||
SHRL CX, R11
|
|
||||||
|
|
||||||
// s++
|
|
||||||
ADDQ $1, SI
|
|
||||||
|
|
||||||
// break out of the inner1 for loop, i.e. continue the outer loop.
|
|
||||||
JMP outer
|
|
||||||
|
|
||||||
emitRemainder:
|
|
||||||
// if nextEmit < len(src) { etc }
|
|
||||||
MOVQ src_len+32(FP), AX
|
|
||||||
ADDQ DX, AX
|
|
||||||
CMPQ R10, AX
|
|
||||||
JEQ encodeBlockEnd
|
|
||||||
|
|
||||||
// d += emitLiteral(dst[d:], src[nextEmit:])
|
|
||||||
//
|
|
||||||
// Push args.
|
|
||||||
MOVQ DI, 0(SP)
|
|
||||||
MOVQ $0, 8(SP) // Unnecessary, as the callee ignores it, but conservative.
|
|
||||||
MOVQ $0, 16(SP) // Unnecessary, as the callee ignores it, but conservative.
|
|
||||||
MOVQ R10, 24(SP)
|
|
||||||
SUBQ R10, AX
|
|
||||||
MOVQ AX, 32(SP)
|
|
||||||
MOVQ AX, 40(SP) // Unnecessary, as the callee ignores it, but conservative.
|
|
||||||
|
|
||||||
// Spill local variables (registers) onto the stack; call; unspill.
|
|
||||||
MOVQ DI, 80(SP)
|
|
||||||
CALL ·emitLiteral(SB)
|
|
||||||
MOVQ 80(SP), DI
|
|
||||||
|
|
||||||
// Finish the "d +=" part of "d += emitLiteral(etc)".
|
|
||||||
ADDQ 48(SP), DI
|
|
||||||
|
|
||||||
encodeBlockEnd:
|
|
||||||
MOVQ dst_base+0(FP), AX
|
|
||||||
SUBQ AX, DI
|
|
||||||
MOVQ DI, d+48(FP)
|
|
||||||
RET
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue