Imported Upstream version 0.8

This commit is contained in:
Sébastien Delafond
2014-10-04 17:09:23 +02:00
parent 8faca75d06
commit 9666e1cf41
354 changed files with 29778 additions and 2868 deletions
+5 -2
View File
@@ -2,13 +2,16 @@ language: go
go:
- 1.2.1
- 1.3
- 1.3.1
- tip
env:
global:
- secure: "YSwtFrMqh4oUvdSQTXBXMHHLWeQgyNEL23ChIZwU0nuDGIcQZ65kipu0PzefedtUbK4ieC065YCUi4UDDh6gPotB/Wu1pnYg3dyQ7rFvhaVYAAUEpajAdXZhlx+7+J8a4FZMeC/kqiahxoRgLbthF9019ouIqhGB9zHKI6/yZwc="
- secure: "V7OjWrfQ8UbktgT036jYQPb/7GJT3Ol9LObDr8FYlzsQ+F1uj2wLac6ePuxcOS4FwWOJinWGM1h+JiFkbxbyFqfRNJ0jj0O2p93QyDojxFVOn1mXqqvV66KFqAWR2Vzkny/gDvj8LTvdB1cgAIm2FNOkQc6E1BFnyWS2sN9ea5E="
before_install:
- sudo apt-get update -qq
- sudo apt-get install -y python-boto
install:
- make prepare
+2 -1
View File
@@ -3,4 +3,5 @@ List of contributors, in chronological order:
* Andrey Smirnov (https://github.com/smira)
* Sebastien Binet (https://github.com/sbinet)
* Ryan Uber (https://github.com/ryanuber)
* Simon Aquino (https://github.com/simonaquino)
* Simon Aquino (https://github.com/simonaquino)
* Vincent Batoufflet (https://github.com/vbatoufflet)
+6 -3
View File
@@ -4,11 +4,14 @@ gom 'code.google.com/p/gographviz', :commit => '454bc64fdfa2'
gom 'code.google.com/p/mxk/go1/flowcontrol', :commit => '5ff2502e2556'
gom 'code.google.com/p/snappy-go/snappy', :commit => '12e4b4183793'
gom 'github.com/cheggaaa/pb', :commit => '74be7a1388046f374ac36e93d46f5d56e856f827'
gom 'github.com/mitchellh/goamz/s3', :commit => '55f224c07975fddef9d2116600c664e30df3d594'
gom 'github.com/vaughan0/go-ini', :commit => 'a98ad7ee00ec53921f08832bc06ecf7fd600e6a1'
gom 'github.com/mattn/go-shellwords', :commit => 'c7ca6f94add751566a61cf2199e1de78d4c3eee4'
gom 'github.com/mitchellh/goamz/s3', :commit => 'e7664b32019f31fd1bdf33f9e85f28722f700405'
gom 'github.com/mkrautz/goar', :commit => '36eb5f3452b1283a211fa35bc00c646fd0db5c4b'
gom 'github.com/smira/commander', :commit => 'f408b00e68d5d6e21b9f18bd310978dafc604e47'
gom 'github.com/smira/flag', :commit => '0d0aac2addb39050f45e92c5a6252926096dc841'
gom 'github.com/syndtr/goleveldb/leveldb', :commit => '9888007'
gom 'github.com/smira/flag', :commit => '357ed3e599ffcbd4aeaa828e1d10da2df3ea5107'
gom 'github.com/smira/go-ftp-protocol/protocol', :commit => '066b75c2b70dca7ae10b1b88b47534a3c31ccfaa'
gom 'github.com/syndtr/goleveldb/leveldb', :commit => 'e2fa4e6ac1cc41a73bc9fd467878ecbf65df5cc3'
gom 'github.com/ugorji/go/codec', :commit => '71c2886f5a673a35f909803f38ece5810165097b'
gom 'github.com/wsxiaoys/terminal/color', :commit => '5668e431776a7957528361f90ce828266c69ed08'
+1
View File
@@ -79,5 +79,6 @@ src-package:
rm -rf aptly-$(VERSION)/src/github.com/smira/aptly/_vendor/{pkg,bin}
tar cyf aptly-$(VERSION)-src.tar.bz2 aptly-$(VERSION)
rm -rf aptly-$(VERSION)
curl -T aptly-$(VERSION)-src.tar.bz2 -usmira:$(BINTRAY_KEY) https://api.bintray.com/content/smira/aptly/aptly/$(VERSION)/$(VERSION)/aptly-$(VERSION)-src.tar.bz2
.PHONY: coverage.out
+6 -4
View File
@@ -11,9 +11,11 @@ aptly
.. image:: http://gobuild.io/badge/github.com/smira/aptly/download.png
:target: http://gobuild.io/github.com/smira/aptly
Aptly is a swiss army knife for Debian repository management.
.. image:: http://www.aptly.info/img/aptly_logo.png
:target: http://www.aptly.info/
Documentation is available at `http://www.aptly.info/ <http://www.aptly.info/>`_. For support use
mailing list `aptly-discuss <https://groups.google.com/forum/#!forum/aptly-discuss>`_.
@@ -24,14 +26,14 @@ Aptly features: ("+" means planned features)
* publish snapshot as Debian repository, ready to be consumed by apt
* controlled update of one or more packages in snapshot from upstream mirror, tracking dependencies
* merge two or more snapshots into one
* filter repository by search query, pulling dependencies when required (+)
* publish self-made packages as Debian repositories (+)
* filter repository by search query, pulling dependencies when required
* publish self-made packages as Debian repositories
* mirror repositories "as-is" (without resigning with user's key) (+)
* support for yum repositories (+)
Current limitations:
* debian-installer and translations not supported yet
* translations are not supported yet
Download
--------
@@ -0,0 +1,13 @@
Copyright (c) 2011-2013, Julien Laffaye <jlaffaye@FreeBSD.org>
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,7 @@
# goftp #
A FTP client package for Go
## Documentation ##
http://godoc.org/github.com/jlaffaye/ftp
@@ -0,0 +1,132 @@
package ftp
import (
"bytes"
"io/ioutil"
"testing"
)
const (
testData = "Just some text"
testDir = "mydir"
)
func TestConn(t *testing.T) {
c, err := Connect("localhost:21")
if err != nil {
t.Fatal(err)
}
err = c.Login("anonymous", "anonymous")
if err != nil {
t.Fatal(err)
}
err = c.NoOp()
if err != nil {
t.Error(err)
}
data := bytes.NewBufferString(testData)
err = c.Stor("test", data)
if err != nil {
t.Error(err)
}
_, err = c.List(".")
if err != nil {
t.Error(err)
}
err = c.Rename("test", "tset")
if err != nil {
t.Error(err)
}
r, err := c.Retr("tset")
if err != nil {
t.Error(err)
} else {
buf, err := ioutil.ReadAll(r)
if err != nil {
t.Error(err)
}
if string(buf) != testData {
t.Errorf("'%s'", buf)
}
r.Close()
}
r, err = c.Retr("tset")
if err != nil {
t.Error(err)
} else {
r.Close()
}
err = c.Delete("tset")
if err != nil {
t.Error(err)
}
err = c.MakeDir(testDir)
if err != nil {
t.Error(err)
}
err = c.ChangeDir(testDir)
if err != nil {
t.Error(err)
}
dir, err := c.CurrentDir()
if err != nil {
t.Error(err)
} else {
if dir != "/"+testDir {
t.Error("Wrong dir: " + dir)
}
}
err = c.ChangeDirToParent()
if err != nil {
t.Error(err)
}
err = c.RemoveDir(testDir)
if err != nil {
t.Error(err)
}
err = c.Logout()
if err != nil {
t.Error(err)
}
c.Quit()
err = c.NoOp()
if err == nil {
t.Error("Expected error")
}
}
// ftp.mozilla.org uses multiline 220 response
func TestConn2(t *testing.T) {
c, err := Connect("ftp.mozilla.org:21")
if err != nil {
t.Fatal(err)
}
err = c.Login("anonymous", "anonymous")
if err != nil {
t.Fatal(err)
}
_, err = c.List(".")
if err != nil {
t.Error(err)
}
c.Quit()
}
@@ -0,0 +1,491 @@
// Package ftp implements a FTP client as described in RFC 959.
package ftp
import (
"bufio"
"errors"
"fmt"
"io"
"net"
"net/textproto"
"strconv"
"strings"
"time"
)
// EntryType describes the different types of an Entry.
type EntryType int
const (
EntryTypeFile EntryType = iota
EntryTypeFolder
EntryTypeLink
)
// ServerConn represents the connection to a remote FTP server.
type ServerConn struct {
conn *textproto.Conn
host string
features map[string]string
}
// Entry describes a file and is returned by List().
type Entry struct {
Name string
Type EntryType
Size uint64
Time time.Time
}
// response represent a data-connection
type response struct {
conn net.Conn
c *ServerConn
}
// Connect initializes the connection to the specified ftp server address.
//
// It is generally followed by a call to Login() as most FTP commands require
// an authenticated user.
func Connect(addr string) (*ServerConn, error) {
conn, err := textproto.Dial("tcp", addr)
if err != nil {
return nil, err
}
a := strings.SplitN(addr, ":", 2)
c := &ServerConn{
conn: conn,
host: a[0],
features: make(map[string]string),
}
_, _, err = c.conn.ReadResponse(StatusReady)
if err != nil {
c.Quit()
return nil, err
}
err = c.feat()
if err != nil {
c.Quit()
return nil, err
}
return c, nil
}
// Login authenticates the client with specified user and password.
//
// "anonymous"/"anonymous" is a common user/password scheme for FTP servers
// that allows anonymous read-only accounts.
func (c *ServerConn) Login(user, password string) error {
code, message, err := c.cmd(-1, "USER %s", user)
if err != nil {
return err
}
switch code {
case StatusLoggedIn:
case StatusUserOK:
_, _, err = c.cmd(StatusLoggedIn, "PASS %s", password)
if err != nil {
return err
}
default:
return errors.New(message)
}
// Switch to binary mode
_, _, err = c.cmd(StatusCommandOK, "TYPE I")
if err != nil {
return err
}
return nil
}
// feat issues a FEAT FTP command to list the additional commands supported by
// the remote FTP server.
// FEAT is described in RFC 2389
func (c *ServerConn) feat() error {
code, message, err := c.cmd(-1, "FEAT")
if err != nil {
return err
}
if code != StatusSystem {
// The server does not support the FEAT command. This is not an
// error: we consider that there is no additional feature.
return nil
}
lines := strings.Split(message, "\n")
for _, line := range lines {
if !strings.HasPrefix(line, " ") {
continue
}
line = strings.TrimSpace(line)
featureElements := strings.SplitN(line, " ", 2)
command := featureElements[0]
var commandDesc string
if len(featureElements) == 2 {
commandDesc = featureElements[1]
}
c.features[command] = commandDesc
}
return nil
}
// epsv issues an "EPSV" command to get a port number for a data connection.
func (c *ServerConn) epsv() (port int, err error) {
_, line, err := c.cmd(StatusExtendedPassiveMode, "EPSV")
if err != nil {
return
}
start := strings.Index(line, "|||")
end := strings.LastIndex(line, "|")
if start == -1 || end == -1 {
err = errors.New("Invalid EPSV response format")
return
}
port, err = strconv.Atoi(line[start+3 : end])
return
}
// pasv issues a "PASV" command to get a port number for a data connection.
func (c *ServerConn) pasv() (port int, err error) {
_, line, err := c.cmd(StatusPassiveMode, "PASV")
if err != nil {
return
}
// PASV response format : 227 Entering Passive Mode (h1,h2,h3,h4,p1,p2).
start := strings.Index(line, "(")
end := strings.LastIndex(line, ")")
if start == -1 || end == -1 {
err = errors.New("Invalid EPSV response format")
return
}
// We have to split the response string
pasvData := strings.Split(line[start+1:end], ",")
// Let's compute the port number
portPart1, err1 := strconv.Atoi(pasvData[4])
if err1 != nil {
err = err1
return
}
portPart2, err2 := strconv.Atoi(pasvData[5])
if err2 != nil {
err = err2
return
}
// Recompose port
port = portPart1*256 + portPart2
return
}
// openDataConn creates a new FTP data connection.
func (c *ServerConn) openDataConn() (net.Conn, error) {
var port int
var err error
// If features contains nat6 or EPSV => EPSV
// else -> PASV
_, nat6Supported := c.features["nat6"]
_, epsvSupported := c.features["EPSV"]
if nat6Supported || epsvSupported {
port, err = c.epsv()
if err != nil {
return nil, err
}
} else {
port, err = c.pasv()
if err != nil {
return nil, err
}
}
// Build the new net address string
addr := fmt.Sprintf("%s:%d", c.host, port)
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
return conn, nil
}
// cmd is a helper function to execute a command and check for the expected FTP
// return code
func (c *ServerConn) cmd(expected int, format string, args ...interface{}) (int, string, error) {
_, err := c.conn.Cmd(format, args...)
if err != nil {
return 0, "", err
}
code, line, err := c.conn.ReadResponse(expected)
return code, line, err
}
// cmdDataConn executes a command which require a FTP data connection.
func (c *ServerConn) cmdDataConn(format string, args ...interface{}) (net.Conn, error) {
conn, err := c.openDataConn()
if err != nil {
return nil, err
}
_, err = c.conn.Cmd(format, args...)
if err != nil {
conn.Close()
return nil, err
}
code, msg, err := c.conn.ReadCodeLine(-1)
if err != nil {
conn.Close()
return nil, err
}
if code != StatusAlreadyOpen && code != StatusAboutToSend {
conn.Close()
return nil, &textproto.Error{code, msg}
}
return conn, nil
}
// parseListLine parses the various non-standard format returned by the LIST
// FTP command.
func parseListLine(line string) (*Entry, error) {
fields := strings.Fields(line)
if len(fields) < 9 {
return nil, errors.New("Unsupported LIST line")
}
e := &Entry{}
switch fields[0][0] {
case '-':
e.Type = EntryTypeFile
case 'd':
e.Type = EntryTypeFolder
case 'l':
e.Type = EntryTypeLink
default:
return nil, errors.New("Unknown entry type")
}
if e.Type == EntryTypeFile {
size, err := strconv.ParseUint(fields[4], 10, 0)
if err != nil {
return nil, err
}
e.Size = size
}
var timeStr string
if strings.Contains(fields[7], ":") { // this year
thisYear, _, _ := time.Now().Date()
timeStr = fields[6] + " " + fields[5] + " " + strconv.Itoa(thisYear)[2:4] + " " + fields[7] + " GMT"
} else { // not this year
timeStr = fields[6] + " " + fields[5] + " " + fields[7][2:4] + " " + "00:00" + " GMT"
}
t, err := time.Parse("_2 Jan 06 15:04 MST", timeStr)
if err != nil {
return nil, err
}
e.Time = t
e.Name = strings.Join(fields[8:], " ")
return e, nil
}
// NameList issues an NLST FTP command.
func (c *ServerConn) NameList(path string) (entries []string, err error) {
conn, err := c.cmdDataConn("NLST %s", path)
if err != nil {
return
}
r := &response{conn, c}
defer r.Close()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
entries = append(entries, scanner.Text())
}
if err = scanner.Err(); err != nil {
return entries, err
}
return
}
// List issues a LIST FTP command.
func (c *ServerConn) List(path string) (entries []*Entry, err error) {
conn, err := c.cmdDataConn("LIST %s", path)
if err != nil {
return
}
r := &response{conn, c}
defer r.Close()
bio := bufio.NewReader(r)
for {
line, e := bio.ReadString('\n')
if e == io.EOF {
break
} else if e != nil {
return nil, e
}
entry, err := parseListLine(line)
if err == nil {
entries = append(entries, entry)
}
}
return
}
// ChangeDir issues a CWD FTP command, which changes the current directory to
// the specified path.
func (c *ServerConn) ChangeDir(path string) error {
_, _, err := c.cmd(StatusRequestedFileActionOK, "CWD %s", path)
return err
}
// ChangeDirToParent issues a CDUP FTP command, which changes the current
// directory to the parent directory. This is similar to a call to ChangeDir
// with a path set to "..".
func (c *ServerConn) ChangeDirToParent() error {
_, _, err := c.cmd(StatusRequestedFileActionOK, "CDUP")
return err
}
// CurrentDir issues a PWD FTP command, which Returns the path of the current
// directory.
func (c *ServerConn) CurrentDir() (string, error) {
_, msg, err := c.cmd(StatusPathCreated, "PWD")
if err != nil {
return "", err
}
start := strings.Index(msg, "\"")
end := strings.LastIndex(msg, "\"")
if start == -1 || end == -1 {
return "", errors.New("Unsuported PWD response format")
}
return msg[start+1 : end], nil
}
// Retr issues a RETR FTP command to fetch the specified file from the remote
// FTP server.
//
// The returned ReadCloser must be closed to cleanup the FTP data connection.
func (c *ServerConn) Retr(path string) (io.ReadCloser, error) {
conn, err := c.cmdDataConn("RETR %s", path)
if err != nil {
return nil, err
}
r := &response{conn, c}
return r, nil
}
// Stor issues a STOR FTP command to store a file to the remote FTP server.
// Stor creates the specified file with the content of the io.Reader.
//
// Hint: io.Pipe() can be used if an io.Writer is required.
func (c *ServerConn) Stor(path string, r io.Reader) error {
conn, err := c.cmdDataConn("STOR %s", path)
if err != nil {
return err
}
_, err = io.Copy(conn, r)
conn.Close()
if err != nil {
return err
}
_, _, err = c.conn.ReadResponse(StatusClosingDataConnection)
return err
}
// Rename renames a file on the remote FTP server.
func (c *ServerConn) Rename(from, to string) error {
_, _, err := c.cmd(StatusRequestFilePending, "RNFR %s", from)
if err != nil {
return err
}
_, _, err = c.cmd(StatusRequestedFileActionOK, "RNTO %s", to)
return err
}
// Delete issues a DELE FTP command to delete the specified file from the
// remote FTP server.
func (c *ServerConn) Delete(path string) error {
_, _, err := c.cmd(StatusRequestedFileActionOK, "DELE %s", path)
return err
}
// MakeDir issues a MKD FTP command to create the specified directory on the
// remote FTP server.
func (c *ServerConn) MakeDir(path string) error {
_, _, err := c.cmd(StatusPathCreated, "MKD %s", path)
return err
}
// RemoveDir issues a RMD FTP command to remove the specified directory from
// the remote FTP server.
func (c *ServerConn) RemoveDir(path string) error {
_, _, err := c.cmd(StatusRequestedFileActionOK, "RMD %s", path)
return err
}
// NoOp issues a NOOP FTP command.
// NOOP has no effects and is usually used to prevent the remote FTP server to
// close the otherwise idle connection.
func (c *ServerConn) NoOp() error {
_, _, err := c.cmd(StatusCommandOK, "NOOP")
return err
}
// Logout issues a REIN FTP command to logout the current user.
func (c *ServerConn) Logout() error {
_, _, err := c.cmd(StatusLoggedIn, "REIN")
return err
}
// Quit issues a QUIT FTP command to properly close the connection from the
// remote FTP server.
func (c *ServerConn) Quit() error {
c.conn.Cmd("QUIT")
return c.conn.Close()
}
// Read implements the io.Reader interface on a FTP data connection.
func (r *response) Read(buf []byte) (int, error) {
n, err := r.conn.Read(buf)
return n, err
}
// Close implements the io.Closer interface on a FTP data connection.
func (r *response) Close() error {
err := r.conn.Close()
_, _, err2 := r.c.conn.ReadResponse(StatusClosingDataConnection)
if err2 != nil {
err = err2
}
return err
}
@@ -0,0 +1,63 @@
package ftp
import (
"testing"
"time"
)
var thisYear, _, _ = time.Now().Date()
type line struct {
line string
name string
size uint64
entryType EntryType
time time.Time
}
var listTests = []line{
// UNIX ls -l style
line{"drwxr-xr-x 3 110 1002 3 Dec 02 2009 pub", "pub", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
line{"drwxr-xr-x 3 110 1002 3 Dec 02 2009 p u b", "p u b", 0, EntryTypeFolder, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
line{"-rwxr-xr-x 3 110 1002 1234567 Dec 02 2009 fileName", "fileName", 1234567, EntryTypeFile, time.Date(2009, time.December, 2, 0, 0, 0, 0, time.UTC)},
line{"lrwxrwxrwx 1 root other 7 Jan 25 00:17 bin -> usr/bin", "bin -> usr/bin", 0, EntryTypeLink, time.Date(thisYear, time.January, 25, 0, 17, 0, 0, time.UTC)},
// Microsoft's FTP servers for Windows
line{"---------- 1 owner group 1803128 Jul 10 10:18 ls-lR.Z", "ls-lR.Z", 1803128, EntryTypeFile, time.Date(thisYear, time.July, 10, 10, 18, 0, 0, time.UTC)},
line{"d--------- 1 owner group 0 May 9 19:45 Softlib", "Softlib", 0, EntryTypeFolder, time.Date(thisYear, time.May, 9, 19, 45, 0, 0, time.UTC)},
// WFTPD for MSDOS
line{"-rwxrwxrwx 1 noone nogroup 322 Aug 19 1996 message.ftp", "message.ftp", 322, EntryTypeFile, time.Date(1996, time.August, 19, 0, 0, 0, 0, time.UTC)},
}
// Not supported, at least we should properly return failure
var listTestsFail = []line{
line{"d [R----F--] supervisor 512 Jan 16 18:53 login", "login", 0, EntryTypeFolder, time.Date(thisYear, time.January, 16, 18, 53, 0, 0, time.UTC)},
line{"- [R----F--] rhesus 214059 Oct 20 15:27 cx.exe", "cx.exe", 0, EntryTypeFile, time.Date(thisYear, time.October, 20, 15, 27, 0, 0, time.UTC)},
}
func TestParseListLine(t *testing.T) {
for _, lt := range listTests {
entry, err := parseListLine(lt.line)
if err != nil {
t.Errorf("parseListLine(%v) returned err = %v", lt.line, err)
continue
}
if entry.Name != lt.name {
t.Errorf("parseListLine(%v).Name = '%v', want '%v'", lt.line, entry.Name, lt.name)
}
if entry.Type != lt.entryType {
t.Errorf("parseListLine(%v).EntryType = %v, want %v", lt.line, entry.Type, lt.entryType)
}
if entry.Size != lt.size {
t.Errorf("parseListLine(%v).Size = %v, want %v", lt.line, entry.Size, lt.size)
}
if entry.Time.Unix() != lt.time.Unix() {
t.Errorf("parseListLine(%v).Time = %v, want %v", lt.line, entry.Time, lt.time)
}
}
for _, lt := range listTestsFail {
_, err := parseListLine(lt.line)
if err == nil {
t.Errorf("parseListLine(%v) expected to fail", lt.line)
}
}
}
@@ -0,0 +1,110 @@
package ftp
const (
// Positive Preliminary reply
StatusInitiating = 100
StatusRestartMarker = 110
StatusReadyMinute = 120
StatusAlreadyOpen = 125
StatusAboutToSend = 150
// Positive Completion reply
StatusCommandOK = 200
StatusCommandNotImplemented = 202
StatusSystem = 211
StatusDirectory = 212
StatusFile = 213
StatusHelp = 214
StatusName = 215
StatusReady = 220
StatusClosing = 221
StatusDataConnectionOpen = 225
StatusClosingDataConnection = 226
StatusPassiveMode = 227
StatusLongPassiveMode = 228
StatusExtendedPassiveMode = 229
StatusLoggedIn = 230
StatusLoggedOut = 231
StatusLogoutAck = 232
StatusRequestedFileActionOK = 250
StatusPathCreated = 257
// Positive Intermediate reply
StatusUserOK = 331
StatusLoginNeedAccount = 332
StatusRequestFilePending = 350
// Transient Negative Completion reply
StatusNotAvailable = 421
StatusCanNotOpenDataConnection = 425
StatusTransfertAborted = 426
StatusInvalidCredentials = 430
StatusHostUnavailable = 434
StatusFileActionIgnored = 450
StatusActionAborted = 451
Status452 = 452
// Permanent Negative Completion reply
StatusBadCommand = 500
StatusBadArguments = 501
StatusNotImplemented = 502
StatusBadSequence = 503
StatusNotImplementedParameter = 504
StatusNotLoggedIn = 530
StatusStorNeedAccount = 532
StatusFileUnavailable = 550
StatusPageTypeUnknown = 551
StatusExceededStorage = 552
StatusBadFileName = 553
)
var statusText = map[int]string{
// 200
StatusCommandOK: "Command okay.",
StatusCommandNotImplemented: "Command not implemented, superfluous at this site.",
StatusSystem: "System status, or system help reply.",
StatusDirectory: "Directory status.",
StatusFile: "File status.",
StatusHelp: "Help message.",
StatusName: "",
StatusReady: "Service ready for new user.",
StatusClosing: "Service closing control connection.",
StatusDataConnectionOpen: "Data connection open; no transfer in progress.",
StatusClosingDataConnection: "Closing data connection. Requested file action successful.",
StatusPassiveMode: "Entering Passive Mode.",
StatusLongPassiveMode: "Entering Long Passive Mode.",
StatusExtendedPassiveMode: "Entering Extended Passive Mode.",
StatusLoggedIn: "User logged in, proceed.",
StatusLoggedOut: "User logged out; service terminated.",
StatusLogoutAck: "Logout command noted, will complete when transfer done.",
StatusRequestedFileActionOK: "Requested file action okay, completed.",
StatusPathCreated: "Path created.",
// 300
StatusUserOK: "User name okay, need password.",
StatusLoginNeedAccount: "Need account for login.",
StatusRequestFilePending: "Requested file action pending further information.",
// 400
StatusNotAvailable: "Service not available, closing control connection.",
StatusCanNotOpenDataConnection: "Can't open data connection.",
StatusTransfertAborted: "Connection closed; transfer aborted.",
StatusInvalidCredentials: "Invalid username or password.",
StatusHostUnavailable: "Requested host unavailable.",
StatusFileActionIgnored: "Requested file action not taken.",
StatusActionAborted: "Requested action aborted. Local error in processing.",
Status452: "Insufficient storage space in system.",
// 500
StatusBadCommand: "Command unrecognized.",
StatusBadArguments: "Syntax error in parameters or arguments.",
StatusNotImplemented: "Command not implemented.",
StatusBadSequence: "Bad sequence of commands.",
StatusNotImplementedParameter: "Command not implemented for that parameter.",
StatusNotLoggedIn: "Not logged in.",
StatusStorNeedAccount: "Need account for storing files.",
StatusFileUnavailable: "File unavailable.",
StatusPageTypeUnknown: "Page type unknown.",
StatusExceededStorage: "Exceeded storage allocation.",
StatusBadFileName: "File name not allowed.",
}
@@ -0,0 +1,9 @@
language: go
go:
- tip
before_install:
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- go get code.google.com/p/go.tools/cmd/cover
script:
- $HOME/gopath/bin/goveralls -repotoken 2FMhp57u8LcstKL9B190fLTcEnBtAAiEL
@@ -0,0 +1,40 @@
# go-shellwords
[![Coverage Status](https://coveralls.io/repos/mattn/go-shellwords/badge.png?branch=master)](https://coveralls.io/r/mattn/go-shellwords?branch=master)
[![Build Status](https://travis-ci.org/mattn/go-shellwords.svg?branch=master)](https://travis-ci.org/mattn/go-shellwords)
Parse line as shell words.
## Usage
```go
args, err := shellwords.Parse("./foo --bar=baz")
// args should be ["./foo", "--bar=baz"]
```
```go
os.Setenv("FOO", "bar")
p := shellwords.NewParser()
p.ParseEnv = true
args, err := p.Parse("./foo $FOO")
// args should be ["./foo", "bar"]
```
```go
p := shellwords.NewParser()
p.ParseBacktick = true
args, err := p.Parse("./foo `echo $SHELL`")
// args should be ["./foo", "/bin/bash"]
```
# Thanks
This is based on cpan module [Parse::CommandLine](https://metacpan.org/pod/Parse::CommandLine).
# License
under the MIT License: http://mattn.mit-license.org/2014
# Author
Yasuhiro Matsumoto (a.k.a mattn)
@@ -0,0 +1,129 @@
package shellwords
import (
"errors"
"os"
"regexp"
"strings"
)
var envRe = regexp.MustCompile(`\$({[a-zA-Z0-9_]+}|[a-zA-Z0-9_]+)`)
func isSpace(r rune) bool {
switch r {
case ' ', '\t', '\r', '\n':
return true
}
return false
}
func replaceEnv(s string) string {
return envRe.ReplaceAllStringFunc(s, func(s string) string {
s = s[1:]
if s[0] == '{' {
s = s[1 : len(s)-1]
}
return os.Getenv(s)
})
}
type Parser struct {
ParseEnv bool
ParseBacktick bool
}
func NewParser() *Parser {
return new(Parser)
}
func (p *Parser) Parse(line string) ([]string, error) {
line = strings.TrimSpace(line)
args := []string{}
buf := ""
var escaped, doubleQuoted, singleQuoted, backQuote bool
backtick := ""
for _, r := range line {
if escaped {
buf += string(r)
escaped = false
continue
}
if r == '\\' {
if singleQuoted {
buf += string(r)
} else {
escaped = true
}
continue
}
if isSpace(r) {
if singleQuoted || doubleQuoted || backQuote {
buf += string(r)
backtick += string(r)
} else if buf != "" {
if p.ParseEnv {
buf = replaceEnv(buf)
}
args = append(args, buf)
buf = ""
}
continue
}
switch r {
case '`':
if !singleQuoted && !doubleQuoted {
if p.ParseBacktick {
if backQuote {
out, err := shellRun(backtick)
if err != nil {
return nil, err
}
buf = out
}
backtick = ""
backQuote = !backQuote
continue
}
backtick = ""
backQuote = !backQuote
}
case '"':
if !singleQuoted {
doubleQuoted = !doubleQuoted
continue
}
case '\'':
if !doubleQuoted {
singleQuoted = !singleQuoted
continue
}
}
buf += string(r)
if backQuote {
backtick += string(r)
}
}
if buf != "" {
if p.ParseEnv {
buf = replaceEnv(buf)
}
args = append(args, buf)
}
if escaped || singleQuoted || doubleQuoted || backQuote {
return nil, errors.New("invalid command line string")
}
return args, nil
}
func Parse(line string) ([]string, error) {
return NewParser().Parse(line)
}
@@ -0,0 +1,132 @@
package shellwords
import (
"os"
"reflect"
"testing"
)
var testcases = []struct {
line string
expected []string
}{
{`var --bar=baz`, []string{`var`, `--bar=baz`}},
{`var --bar="baz"`, []string{`var`, `--bar=baz`}},
{`var "--bar=baz"`, []string{`var`, `--bar=baz`}},
{`var "--bar='baz'"`, []string{`var`, `--bar='baz'`}},
{"var --bar=`baz`", []string{`var`, "--bar=`baz`"}},
{`var "--bar=\"baz'"`, []string{`var`, `--bar="baz'`}},
{`var "--bar=\'baz\'"`, []string{`var`, `--bar='baz'`}},
{`var --bar='\'`, []string{`var`, `--bar=\`}},
{`var "--bar baz"`, []string{`var`, `--bar baz`}},
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
{`var --"bar baz"`, []string{`var`, `--bar baz`}},
}
func TestSimple(t *testing.T) {
for _, testcase := range testcases {
args, err := Parse(testcase.line)
if err != nil {
t.Fatalf(err.Error())
}
if !reflect.DeepEqual(args, testcase.expected) {
t.Fatalf("Expected %v, but %v:", testcase.expected, args)
}
}
}
func TestError(t *testing.T) {
_, err := Parse("foo '")
if err == nil {
t.Fatalf("Should be an error")
}
_, err = Parse(`foo "`)
if err == nil {
t.Fatalf("Should be an error")
}
_, err = Parse("foo `")
if err == nil {
t.Fatalf("Should be an error")
}
}
func TestBacktick(t *testing.T) {
goversion, err := shellRun("go version")
if err != nil {
t.Fatalf(err.Error())
}
parser := NewParser()
parser.ParseBacktick = true
args, err := parser.Parse("echo `go version`")
if err != nil {
t.Fatalf(err.Error())
}
expected := []string{"echo", goversion}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %v, but %v:", expected, args)
}
}
func TestBacktickError(t *testing.T) {
parser := NewParser()
parser.ParseBacktick = true
_, err := parser.Parse("echo `go Version`")
if err == nil {
t.Fatalf("Should be an error")
}
}
func TestEnv(t *testing.T) {
os.Setenv("FOO", "bar")
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $FOO")
if err != nil {
t.Fatalf(err.Error())
}
expected := []string{"echo", "bar"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %v, but %v:", expected, args)
}
}
func TestNoEnv(t *testing.T) {
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $BAR")
if err != nil {
t.Fatalf(err.Error())
}
expected := []string{"echo", ""}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %v, but %v:", expected, args)
}
}
func TestDupEnv(t *testing.T) {
os.Setenv("FOO", "bar")
os.Setenv("FOO_BAR", "baz")
parser := NewParser()
parser.ParseEnv = true
args, err := parser.Parse("echo $$FOO$")
if err != nil {
t.Fatalf(err.Error())
}
expected := []string{"echo", "$bar$"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %v, but %v:", expected, args)
}
args, err = parser.Parse("echo $${FOO_BAR}$")
if err != nil {
t.Fatalf(err.Error())
}
expected = []string{"echo", "$baz$"}
if !reflect.DeepEqual(args, expected) {
t.Fatalf("Expected %v, but %v:", expected, args)
}
}
@@ -0,0 +1,19 @@
// +build !windows
package shellwords
import (
"errors"
"os"
"os/exec"
"strings"
)
func shellRun(line string) (string, error) {
shell := os.Getenv("SHELL")
b, err := exec.Command(shell, "-c", line).Output()
if err != nil {
return "", errors.New(err.Error() + ":" + string(b))
}
return strings.TrimSpace(string(b)), nil
}
@@ -0,0 +1,17 @@
package shellwords
import (
"errors"
"os"
"os/exec"
"strings"
)
func shellRun(line string) (string, error) {
shell := os.Getenv("COMSPEC")
b, err := exec.Command(shell, "/c", line).Output()
if err != nil {
return "", errors.New(err.Error() + ":" + string(b))
}
return strings.TrimSpace(string(b)), nil
}
@@ -1,4 +1,41 @@
# goamz - An Amazon Library for Go
# goamz - An Amazon Library for Go
Current API documentation: [![GoDoc](https://godoc.org/github.com/mitchellh/goamz?status.svg)](https://godoc.org/github.com/mitchellh/goamz)
This is a fork of [https://launchpad.net/goamz](https://launchpad.net/goamz)
that adds some missing API calls to certain packages.
This library is *incomplete*, but implements a large amount of the AWS API.
It is heavily used in projects such as
[Terraform](https://github.com/hashicorp/terraform) and
[Packer](https://github.com/mitchellh/packer).
If you find anything missing from this library,
please [file an issue](https://github.com/mitchellh/goamz).
## Example Usage
```go
package main
import (
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/s3"
"log"
"fmt"
)
func main() {
auth, err := aws.EnvAuth()
if err != nil {
log.Fatal(err)
}
client := s3.New(auth, aws.USEast)
resp, err := client.ListBuckets()
if err != nil {
log.Fatal(err)
}
log.Print(fmt.Sprintf("%T %+v", resp.Buckets[0], resp.Buckets[0]))
}
```
@@ -4,12 +4,13 @@ package autoscaling
import (
"encoding/xml"
"github.com/mitchellh/goamz/aws"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/mitchellh/goamz/aws"
)
// The AutoScaling type encapsulates operations operations with the autoscaling endpoint.
@@ -109,11 +110,13 @@ type LoadBalancerName struct {
}
type LaunchConfiguration struct {
ImageId string `xml:"member>ImageId"`
InstanceType string `xml:"member>InstanceType"`
KeyName string `xml:"member>KeyName"`
Name string `xml:"member>LaunchConfigurationName"`
SecurityGroups []SecurityGroup `xml:"member>SecurityGroups"`
IamInstanceProfile string `xml:"member>IamInstanceProfile"`
ImageId string `xml:"member>ImageId"`
InstanceType string `xml:"member>InstanceType"`
KeyName string `xml:"member>KeyName"`
Name string `xml:"member>LaunchConfigurationName"`
SecurityGroups []SecurityGroup `xml:"member>SecurityGroups"`
UserData []byte `xml:"member>UserData"`
}
type AutoScalingGroup struct {
@@ -147,6 +150,7 @@ type CreateAutoScalingGroup struct {
MaxSize int
MinSize int
PlacementGroup string
TerminationPolicies []string
Name string
Tags []Tag
VPCZoneIdentifier []string
@@ -212,6 +216,10 @@ func (autoscaling *AutoScaling) CreateAutoScalingGroup(options *CreateAutoScalin
params["Tag.member."+strconv.Itoa(j+1)+".Value"] = tag.Value
}
for i, v := range options.TerminationPolicies {
params["TerminationPolicies.member."+strconv.Itoa(i+1)] = v
}
if options.VPCZoneIdentifier != nil {
params["VPCZoneIdentifier"] = strings.Join(options.VPCZoneIdentifier, ",")
}
@@ -235,6 +243,7 @@ type CreateLaunchConfiguration struct {
KeyName string
Name string
SecurityGroups []string
UserData string
}
func (autoscaling *AutoScaling) CreateLaunchConfiguration(options *CreateLaunchConfiguration) (resp *SimpleResp, err error) {
@@ -260,6 +269,12 @@ func (autoscaling *AutoScaling) CreateLaunchConfiguration(options *CreateLaunchC
params["SecurityGroups.member."+strconv.Itoa(i+1)] = v
}
if options.UserData != "" {
userData := make([]byte, b64.EncodedLen(len(options.UserData)))
b64.Encode(userData, []byte(options.UserData))
params["UserData"] = string(userData)
}
resp = &SimpleResp{}
err = autoscaling.query(params, resp)
@@ -389,6 +404,7 @@ type UpdateAutoScalingGroup struct {
MaxSize int
MinSize int
PlacementGroup string
TerminationPolicies []string
Name string
VPCZoneIdentifier []string
@@ -441,6 +457,9 @@ func (autoscaling *AutoScaling) UpdateAutoScalingGroup(options *UpdateAutoScalin
if options.PlacementGroup != "" {
params["PlacementGroup"] = options.PlacementGroup
}
for i, v := range options.TerminationPolicies {
params["TerminationPolicies.member."+strconv.Itoa(i+1)] = v
}
if options.VPCZoneIdentifier != nil {
params["VPCZoneIdentifier"] = strings.Join(options.VPCZoneIdentifier, ",")
@@ -1,11 +1,12 @@
package autoscaling_test
import (
"testing"
"github.com/mitchellh/goamz/autoscaling"
"github.com/mitchellh/goamz/aws"
"github.com/mitchellh/goamz/testutil"
. "github.com/motain/gocheck"
"testing"
)
func Test(t *testing.T) {
@@ -44,6 +45,7 @@ func (s *S) Test_CreateAutoScalingGroup(c *C) {
MinSize: 2,
MaxSize: 2,
PlacementGroup: "foobar",
TerminationPolicies: []string{"ClosestToNextInstanceHour", "OldestInstance"},
Name: "foobar",
Tags: []autoscaling.Tag{
autoscaling.Tag{
@@ -60,6 +62,8 @@ func (s *S) Test_CreateAutoScalingGroup(c *C) {
c.Assert(req.Form["Action"], DeepEquals, []string{"CreateAutoScalingGroup"})
c.Assert(req.Form["InstanceId"], DeepEquals, []string{"i-foo"})
c.Assert(req.Form["VPCZoneIdentifier"], DeepEquals, []string{"foo,bar"})
c.Assert(req.Form["TerminationPolicies.member.1"], DeepEquals, []string{"ClosestToNextInstanceHour"})
c.Assert(req.Form["TerminationPolicies.member.2"], DeepEquals, []string{"OldestInstance"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "8d798a29-f083-11e1-bdfb-cb223EXAMPLE")
}
@@ -74,6 +78,7 @@ func (s *S) Test_CreateLaunchConfiguration(c *C) {
InstanceType: "m1.small",
KeyName: "foobar",
Name: "i-141421",
UserData: "#!/bin/bash\necho Hello\n",
}
resp, err := s.autoscaling.CreateLaunchConfiguration(&options)
@@ -82,6 +87,7 @@ func (s *S) Test_CreateLaunchConfiguration(c *C) {
c.Assert(req.Form["Action"], DeepEquals, []string{"CreateLaunchConfiguration"})
c.Assert(req.Form["InstanceType"], DeepEquals, []string{"m1.small"})
c.Assert(req.Form["SecurityGroups.member.1"], DeepEquals, []string{"sg-1111"})
c.Assert(req.Form["UserData"], DeepEquals, []string{"IyEvYmluL2Jhc2gKZWNobyBIZWxsbwo="})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "7c6e177f-f082-11e1-ac58-3714bEXAMPLE")
}
@@ -15,6 +15,8 @@ import (
"fmt"
"io/ioutil"
"os"
"github.com/vaughan0/go-ini"
)
// Region defines the URLs where AWS services may be accessed.
@@ -190,6 +192,23 @@ var SAEast = Region{
"https://route53.amazonaws.com",
}
var CNNorth = Region{
"cn-north-1",
"https://ec2.cn-north-1.amazonaws.com.cn",
"https://s3.cn-north-1.amazonaws.com.cn",
"",
true,
true,
"",
"https://sns.cn-north-1.amazonaws.com.cn",
"https://sqs.cn-north-1.amazonaws.com.cn",
"https://iam.cn-north-1.amazonaws.com.cn",
"https://elasticloadbalancing.cn-north-1.amazonaws.com.cn",
"https://autoscaling.cn-north-1.amazonaws.com.cn",
"https://rds.cn-north-1.amazonaws.com.cn",
"https://route53.amazonaws.com",
}
var Regions = map[string]Region{
APNortheast.Name: APNortheast,
APSoutheast.Name: APSoutheast,
@@ -200,6 +219,7 @@ var Regions = map[string]Region{
USWest2.Name: USWest2,
SAEast.Name: SAEast,
USGovWest.Name: USGovWest,
CNNorth.Name: CNNorth,
}
type Auth struct {
@@ -278,6 +298,13 @@ func GetAuth(accessKey string, secretKey string) (auth Auth, err error) {
return Auth{accessKey, secretKey, ""}, nil
}
// Next try to get auth from the environment
auth, err = SharedAuth()
if err == nil {
// Found auth, return
return
}
// Next try to get auth from the environment
auth, err = EnvAuth()
if err == nil {
@@ -298,8 +325,53 @@ func GetAuth(accessKey string, secretKey string) (auth Auth, err error) {
return
}
// SharedAuth creates an Auth based on shared credentials stored in
// $HOME/.aws/credentials. The AWS_PROFILE environment variables is used to
// select the profile.
func SharedAuth() (auth Auth, err error) {
var profileName = os.Getenv("AWS_PROFILE")
if profileName == "" {
profileName = "default"
}
var credentialsFile = os.Getenv("AWS_CREDENTIAL_FILE")
if credentialsFile == "" {
var homeDir = os.Getenv("HOME")
if homeDir == "" {
err = errors.New("Could not get HOME")
return
}
credentialsFile = homeDir + "/.aws/credentials"
}
file, err := ini.LoadFile(credentialsFile)
if err != nil {
err = errors.New("Couldn't parse AWS credentials file")
return
}
var profile = file[profileName]
if profile == nil {
err = errors.New("Couldn't find profile in AWS credentials file")
return
}
auth.AccessKey = profile["aws_access_key_id"]
auth.SecretKey = profile["aws_secret_access_key"]
if auth.AccessKey == "" {
err = errors.New("AWS_ACCESS_KEY_ID not found in environment in credentials file")
}
if auth.SecretKey == "" {
err = errors.New("AWS_SECRET_ACCESS_KEY not found in credentials file")
}
return
}
// EnvAuth creates an Auth based on environment information.
// The AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY environment
// For accounts that require a security token, it is read from AWS_SECURITY_TOKEN
// variables are used.
func EnvAuth() (auth Auth, err error) {
auth.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
@@ -311,6 +383,9 @@ func EnvAuth() (auth Auth, err error) {
if auth.SecretKey == "" {
auth.SecretKey = os.Getenv("AWS_SECRET_KEY")
}
auth.Token = os.Getenv("AWS_SECURITY_TOKEN")
if auth.AccessKey == "" {
err = errors.New("AWS_ACCESS_KEY_ID or AWS_ACCESS_KEY not found in environment")
}
@@ -3,6 +3,7 @@ package aws_test
import (
"github.com/mitchellh/goamz/aws"
. "github.com/motain/gocheck"
"io/ioutil"
"os"
"strings"
"testing"
@@ -30,6 +31,110 @@ func (s *S) TearDownTest(c *C) {
}
}
func (s *S) TestSharedAuthNoHome(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "foo")
_, err := aws.SharedAuth()
c.Assert(err, ErrorMatches, "Could not get HOME")
}
func (s *S) TestSharedAuthNoCredentialsFile(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "foo")
os.Setenv("HOME", "/tmp")
_, err := aws.SharedAuth()
c.Assert(err, ErrorMatches, "Couldn't parse AWS credentials file")
}
func (s *S) TestSharedAuthNoProfileInFile(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "foo")
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\n"), 0644)
os.Setenv("HOME", d)
_, err = aws.SharedAuth()
c.Assert(err, ErrorMatches, "Couldn't find profile in AWS credentials file")
}
func (s *S) TestSharedAuthNoKeysInProfile(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "bar")
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\nawsaccesskeyid = AK.."), 0644)
os.Setenv("HOME", d)
_, err = aws.SharedAuth()
c.Assert(err, ErrorMatches, "AWS_SECRET_ACCESS_KEY not found in credentials file")
}
func (s *S) TestSharedAuthDefaultCredentials(c *C) {
os.Clearenv()
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[default]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644)
os.Setenv("HOME", d)
auth, err := aws.SharedAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestSharedAuth(c *C) {
os.Clearenv()
os.Setenv("AWS_PROFILE", "bar")
d, err := ioutil.TempDir("", "")
if err != nil {
panic(err)
}
defer os.RemoveAll(d)
err = os.Mkdir(d+"/.aws", 0755)
if err != nil {
panic(err)
}
ioutil.WriteFile(d+"/.aws/credentials", []byte("[bar]\naws_access_key_id = access\naws_secret_access_key = secret\n"), 0644)
os.Setenv("HOME", d)
auth, err := aws.SharedAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestEnvAuthNoSecret(c *C) {
os.Clearenv()
_, err := aws.EnvAuth()
@@ -52,6 +157,16 @@ func (s *S) TestEnvAuth(c *C) {
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access"})
}
func (s *S) TestEnvAuthWithToken(c *C) {
os.Clearenv()
os.Setenv("AWS_SECRET_ACCESS_KEY", "secret")
os.Setenv("AWS_ACCESS_KEY_ID", "access")
os.Setenv("AWS_SECURITY_TOKEN", "token")
auth, err := aws.EnvAuth()
c.Assert(err, IsNil)
c.Assert(auth, Equals, aws.Auth{SecretKey: "secret", AccessKey: "access", Token: "token"})
}
func (s *S) TestEnvAuthAlt(c *C) {
os.Clearenv()
os.Setenv("AWS_SECRET_KEY", "secret")
@@ -178,7 +178,7 @@ func buildError(r *http.Response) error {
err.RequestId = errors.RequestId
err.StatusCode = r.StatusCode
if err.Message == "" {
err.Message = r.Status
err.Message = err.Code
}
return &err
}
@@ -220,6 +220,8 @@ func addBlockDeviceParams(prename string, params map[string]string, blockdevices
}
if k.DeleteOnTermination {
params[prefix+"Ebs.DeleteOnTermination"] = "true"
} else {
params[prefix+"Ebs.DeleteOnTermination"] = "false"
}
if k.Encrypted {
params[prefix+"Ebs.Encrypted"] = "true"
@@ -253,6 +255,7 @@ type RunInstances struct {
SubnetId string
AssociatePublicIpAddress bool
DisableAPITermination bool
EbsOptimized bool
ShutdownBehavior string
PrivateIPAddress string
BlockDevices []BlockDeviceMapping
@@ -269,6 +272,15 @@ type RunInstancesResp struct {
Instances []Instance `xml:"instancesSet>item"`
}
// BlockDevice represents the association of a block device with an instance.
type BlockDevice struct {
DeviceName string `xml:"deviceName"`
VolumeId string `xml:"ebs>volumeId"`
Status string `xml:"ebs>status"`
AttachTime string `xml:"ebs>attachTime"`
DeleteOnTermination bool `xml:"ebs>deleteOnTermination"`
}
// Instance encapsulates a running instance in EC2.
//
// See http://goo.gl/OCH8a for more details.
@@ -296,6 +308,8 @@ type Instance struct {
LaunchTime time.Time `xml:"launchTime"`
SourceDestCheck bool `xml:"sourceDestCheck"`
SecurityGroups []SecurityGroup `xml:"groupSet>item"`
EbsOptimized string `xml:"ebsOptimized"`
BlockDevices []BlockDevice `xml:"blockDeviceMapping>item"`
}
// RunInstances starts new instances in EC2.
@@ -392,6 +406,9 @@ func (ec2 *EC2) RunInstances(options *RunInstances) (resp *RunInstancesResp, err
if options.DisableAPITermination {
params["DisableApiTermination"] = "true"
}
if options.EbsOptimized {
params["EbsOptimized"] = "true"
}
if options.ShutdownBehavior != "" {
params["InstanceInitiatedShutdownBehavior"] = options.ShutdownBehavior
}
@@ -419,6 +436,78 @@ func clientToken() (string, error) {
return hex.EncodeToString(buf), nil
}
// ----------------------------------------------------------------------------
// Instance events and status functions and types.
// The DescribeInstanceStatus type encapsulates options for the respective request in EC2.
//
// See http://goo.gl/DFySJY for more details.
type EventsSet struct {
Code string `xml:"code"`
Description string `xml:"description"`
NotBefore string `xml:"notBefore"`
NotAfter string `xml:"notAfter"`
}
type StatusDetails struct {
Name string `xml:"name"`
Status string `xml:"status"`
ImpairedSince string `xml:"impairedSince"`
}
type Status struct {
Status string `xml:"status"`
Details []StatusDetails `xml:"details>item"`
}
type InstanceStatusSet struct {
InstanceId string `xml:"instanceId"`
AvailabilityZone string `xml:"availabilityZone"`
InstanceState InstanceState `xml:"instanceState"`
SystemStatus Status `xml:"systemStatus"`
InstanceStatus Status `xml:"instanceStatus"`
Events []EventsSet `xml:"eventsSet>item"`
}
type DescribeInstanceStatusResp struct {
RequestId string `xml:"requestId"`
InstanceStatus []InstanceStatusSet `xml:"instanceStatusSet>item"`
}
type DescribeInstanceStatus struct {
InstanceIds []string
IncludeAllInstances bool
MaxResults int64
NextToken string
}
func (ec2 *EC2) DescribeInstanceStatus(options *DescribeInstanceStatus, filter *Filter) (resp *DescribeInstanceStatusResp, err error) {
params := makeParams("DescribeInstanceStatus")
if options.IncludeAllInstances {
params["IncludeAllInstances"] = "true"
}
if len(options.InstanceIds) > 0 {
addParamsList(params, "InstanceIds", options.InstanceIds)
}
if options.MaxResults > 0 {
params["MaxResults"] = strconv.FormatInt(options.MaxResults, 10)
}
if options.NextToken != "" {
params["NextToken"] = options.NextToken
}
if filter != nil {
filter.addParams(params)
}
resp = &DescribeInstanceStatusResp{}
err = ec2.query(params, resp)
if err != nil {
return nil, err
}
return
}
// ----------------------------------------------------------------------------
// Spot Instance management functions and types.
@@ -478,6 +567,12 @@ type SpotLaunchSpec struct {
BlockDevices []BlockDeviceMapping `xml:"blockDeviceMapping>item"`
}
type SpotStatus struct {
Code string `xml:"code"`
UpdateTime string `xml:"updateTime"`
Message string `xml:"message"`
}
type SpotRequestResult struct {
SpotRequestId string `xml:"spotInstanceRequestId"`
SpotPrice string `xml:"spotPrice"`
@@ -485,6 +580,7 @@ type SpotRequestResult struct {
AvailZone string `xml:"launchedAvailabilityZone"`
InstanceId string `xml:"instanceId"`
State string `xml:"state"`
Status SpotStatus `xml:"status"`
SpotLaunchSpec SpotLaunchSpec `xml:"launchSpecification"`
CreateTime string `xml:"createTime"`
Tags []Tag `xml:"tagSet>item"`
@@ -636,6 +732,61 @@ func (ec2 *EC2) CancelSpotRequests(spotrequestIds []string) (resp *CancelSpotReq
return
}
type DescribeSpotPriceHistory struct {
InstanceType []string
ProductDescription []string
AvailabilityZone string
StartTime, EndTime time.Time
}
// Response to a DescribeSpotPriceHisotyr request.
//
// See http://goo.gl/3BKHj for more details.
type DescribeSpotPriceHistoryResp struct {
RequestId string `xml:"requestId"`
History []SpotPriceHistory `xml:"spotPriceHistorySet>item"`
}
type SpotPriceHistory struct {
InstanceType string `xml:"instanceType"`
ProductDescription string `xml:"productDescription"`
SpotPrice string `xml:"spotPrice"`
Timestamp time.Time `xml:"timestamp"`
AvailabilityZone string `xml:"availabilityZone"`
}
// DescribeSpotPriceHistory gets the spot pricing history.
//
// See http://goo.gl/3BKHj for more details.
func (ec2 *EC2) DescribeSpotPriceHistory(o *DescribeSpotPriceHistory) (resp *DescribeSpotPriceHistoryResp, err error) {
params := makeParams("DescribeSpotPriceHistory")
if o.AvailabilityZone != "" {
params["AvailabilityZone"] = o.AvailabilityZone
}
if !o.StartTime.IsZero() {
params["StartTime"] = o.StartTime.In(time.UTC).Format(time.RFC3339)
}
if !o.EndTime.IsZero() {
params["EndTime"] = o.EndTime.In(time.UTC).Format(time.RFC3339)
}
if len(o.InstanceType) > 0 {
addParamsList(params, "InstanceType", o.InstanceType)
}
if len(o.ProductDescription) > 0 {
addParamsList(params, "ProductDescription", o.ProductDescription)
}
resp = &DescribeSpotPriceHistoryResp{}
err = ec2.query(params, resp)
if err != nil {
return nil, err
}
return
}
// Response to a TerminateInstances request.
//
// See http://goo.gl/3BKHj for more details.
@@ -1834,6 +1985,29 @@ func (ec2 *EC2) CreateTags(resourceIds []string, tags []Tag) (resp *SimpleResp,
return resp, nil
}
type TagsResp struct {
RequestId string `xml:"requestId"`
Tags []ResourceTag `xml:"tagSet>item"`
}
type ResourceTag struct {
Tag
ResourceId string `xml:"resourceId"`
ResourceType string `xml:"resourceType"`
}
func (ec2 *EC2) Tags(filter *Filter) (*TagsResp, error) {
params := makeParams("DescribeTags")
filter.addParams(params)
resp := &TagsResp{}
if err := ec2.query(params, resp); err != nil {
return nil, err
}
return resp, nil
}
// Response to a StartInstances request.
//
// See http://goo.gl/awKeF for more details.
@@ -2006,6 +2180,27 @@ type CreateVpcResp struct {
VPC VPC `xml:"vpc"`
}
// The ModifyVpcAttribute request parameters.
//
// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details.
type ModifyVpcAttribute struct {
EnableDnsSupport bool
EnableDnsHostnames bool
SetEnableDnsSupport bool
SetEnableDnsHostnames bool
}
// Response to a DescribeVpcAttribute request.
//
// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details.
type VpcAttributeResp struct {
RequestId string `xml:"requestId"`
VpcId string `xml:"vpcId"`
EnableDnsSupport bool `xml:"enableDnsSupport>value"`
EnableDnsHostnames bool `xml:"enableDnsHostnames>value"`
}
// CreateInternetGateway request parameters.
//
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-CreateInternetGateway.html
@@ -2074,6 +2269,19 @@ type CreateSubnetResp struct {
Subnet Subnet `xml:"subnet"`
}
// The ModifySubnetAttribute request parameters
//
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-ModifySubnetAttribute.html
type ModifySubnetAttribute struct {
SubnetId string
MapPublicIpOnLaunch bool
}
type ModifySubnetAttributeResp struct {
RequestId string `xml:"requestId"`
Return bool `xml:"return"`
}
// Response to a DescribeInternetGateways request.
type InternetGatewaysResp struct {
RequestId string `xml:"requestId"`
@@ -2208,6 +2416,49 @@ func (ec2 *EC2) DescribeVpcs(ids []string, filter *Filter) (resp *VpcsResp, err
return
}
// VpcAttribute describes an attribute of a VPC.
// You can specify only one attribute at a time.
// Valid attributes are:
// enableDnsSupport | enableDnsHostnames
//
// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-DescribeVpcAttribute.html for more details.
func (ec2 *EC2) VpcAttribute(vpcId, attribute string) (resp *VpcAttributeResp, err error) {
params := makeParams("DescribeVpcAttribute")
params["VpcId"] = vpcId
params["Attribute"] = attribute
resp = &VpcAttributeResp{}
err = ec2.query(params, resp)
if err != nil {
return nil, err
}
return
}
// ModifyVpcAttribute modifies the specified attribute of the specified VPC.
//
// See http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/index.html?ApiReference-query-ModifyVpcAttribute.html for more details.
func (ec2 *EC2) ModifyVpcAttribute(vpcId string, options *ModifyVpcAttribute) (*SimpleResp, error) {
params := makeParams("ModifyVpcAttribute")
params["VpcId"] = vpcId
if options.SetEnableDnsSupport {
params["EnableDnsSupport.Value"] = strconv.FormatBool(options.EnableDnsSupport)
}
if options.SetEnableDnsHostnames {
params["EnableDnsHostnames.Value"] = strconv.FormatBool(options.EnableDnsHostnames)
}
resp := &SimpleResp{}
if err := ec2.query(params, resp); err != nil {
return nil, err
}
return resp, nil
}
// Create a new subnet.
func (ec2 *EC2) CreateSubnet(options *CreateSubnet) (resp *CreateSubnetResp, err error) {
params := makeParams("CreateSubnet")
@@ -2237,6 +2488,26 @@ func (ec2 *EC2) DeleteSubnet(id string) (resp *SimpleResp, err error) {
return
}
// ModifySubnetAttribute
//
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-ModifySubnetAttribute.html
func (ec2 *EC2) ModifySubnetAttribute(options *ModifySubnetAttribute) (resp *ModifySubnetAttributeResp, err error) {
params := makeParams("ModifySubnetAttribute")
params["SubnetId"] = options.SubnetId
if options.MapPublicIpOnLaunch {
params["MapPublicIpOnLaunch.Value"] = "true"
} else {
params["MapPublicIpOnLaunch.Value"] = "false"
}
resp = &ModifySubnetAttributeResp{}
err = ec2.query(params, resp)
if err != nil {
return nil, err
}
return
}
// DescribeSubnets
//
// http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeSubnets.html
@@ -95,13 +95,13 @@ func (s *S) TestRunInstancesErrorWithoutXML(c *C) {
testServer.WaitRequest()
c.Assert(resp, IsNil)
c.Assert(err, ErrorMatches, "500 Internal Server Error")
c.Assert(err, ErrorMatches, "")
ec2err, ok := err.(*ec2.Error)
c.Assert(ok, Equals, true)
c.Assert(ec2err.StatusCode, Equals, 500)
c.Assert(ec2err.Code, Equals, "")
c.Assert(ec2err.Message, Equals, "500 Internal Server Error")
c.Assert(ec2err.Message, Equals, "")
c.Assert(ec2err.RequestId, Equals, "")
}
@@ -114,13 +114,13 @@ func (s *S) TestRequestSpotInstancesErrorWithoutXML(c *C) {
testServer.WaitRequest()
c.Assert(resp, IsNil)
c.Assert(err, ErrorMatches, "500 Internal Server Error")
c.Assert(err, ErrorMatches, "")
ec2err, ok := err.(*ec2.Error)
c.Assert(ok, Equals, true)
c.Assert(ec2err.StatusCode, Equals, 500)
c.Assert(ec2err.Code, Equals, "")
c.Assert(ec2err.Message, Equals, "500 Internal Server Error")
c.Assert(ec2err.Message, Equals, "")
c.Assert(ec2err.RequestId, Equals, "")
}
@@ -140,6 +140,7 @@ func (s *S) TestRunInstancesExample(c *C) {
Monitoring: true,
SubnetId: "subnet-id",
DisableAPITermination: true,
EbsOptimized: true,
ShutdownBehavior: "terminate",
PrivateIPAddress: "10.0.0.25",
BlockDevices: []ec2.BlockDeviceMapping{
@@ -168,6 +169,7 @@ func (s *S) TestRunInstancesExample(c *C) {
c.Assert(req.Form["Monitoring.Enabled"], DeepEquals, []string{"true"})
c.Assert(req.Form["SubnetId"], DeepEquals, []string{"subnet-id"})
c.Assert(req.Form["DisableApiTermination"], DeepEquals, []string{"true"})
c.Assert(req.Form["EbsOptimized"], DeepEquals, []string{"true"})
c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], DeepEquals, []string{"terminate"})
c.Assert(req.Form["PrivateIpAddress"], DeepEquals, []string{"10.0.0.25"})
c.Assert(req.Form["BlockDeviceMapping.1.DeviceName"], DeepEquals, []string{"/dev/sdb"})
@@ -266,6 +268,9 @@ func (s *S) TestRequestSpotInstancesExample(c *C) {
c.Assert(resp.SpotRequestResults[0].SpotPrice, Equals, "0.5")
c.Assert(resp.SpotRequestResults[0].State, Equals, "open")
c.Assert(resp.SpotRequestResults[0].SpotLaunchSpec.ImageId, Equals, "ami-1a2b3c4d")
c.Assert(resp.SpotRequestResults[0].Status.Code, Equals, "pending-evaluation")
c.Assert(resp.SpotRequestResults[0].Status.UpdateTime, Equals, "2008-05-07T12:51:50.000Z")
c.Assert(resp.SpotRequestResults[0].Status.Message, Equals, "Your Spot request has been submitted for review, and is pending evaluation.")
}
func (s *S) TestCancelSpotRequestsExample(c *C) {
@@ -301,6 +306,7 @@ func (s *S) TestTerminateInstancesExample(c *C) {
c.Assert(req.Form["Monitoring.Enabled"], IsNil)
c.Assert(req.Form["SubnetId"], IsNil)
c.Assert(req.Form["DisableApiTermination"], IsNil)
c.Assert(req.Form["EbsOptimized"], IsNil)
c.Assert(req.Form["InstanceInitiatedShutdownBehavior"], IsNil)
c.Assert(req.Form["PrivateIpAddress"], IsNil)
@@ -334,6 +340,9 @@ func (s *S) TestDescribeSpotRequestsExample(c *C) {
c.Assert(resp.SpotRequestResults[0].State, Equals, "active")
c.Assert(resp.SpotRequestResults[0].SpotPrice, Equals, "0.5")
c.Assert(resp.SpotRequestResults[0].SpotLaunchSpec.ImageId, Equals, "ami-1a2b3c4d")
c.Assert(resp.SpotRequestResults[0].Status.Code, Equals, "fulfilled")
c.Assert(resp.SpotRequestResults[0].Status.UpdateTime, Equals, "2008-05-07T12:51:50.000Z")
c.Assert(resp.SpotRequestResults[0].Status.Message, Equals, "Your Spot request is fulfilled.")
}
func (s *S) TestDescribeInstancesExample1(c *C) {
@@ -366,6 +375,13 @@ func (s *S) TestDescribeInstancesExample1(c *C) {
c.Assert(r0i.PrivateDNSName, Equals, "domU-12-31-39-10-56-34.compute-1.internal")
c.Assert(r0i.DNSName, Equals, "ec2-174-129-165-232.compute-1.amazonaws.com")
c.Assert(r0i.AvailZone, Equals, "us-east-1b")
b0 := r0i.BlockDevices[0]
c.Assert(b0.DeviceName, Equals, "/dev/sda1")
c.Assert(b0.VolumeId, Equals, "vol-a082c1c9")
c.Assert(b0.Status, Equals, "attached")
c.Assert(b0.AttachTime, Equals, "2010-08-17T01:15:21.000Z")
c.Assert(b0.DeleteOnTermination, Equals, false)
}
func (s *S) TestDescribeInstancesExample2(c *C) {
@@ -1041,6 +1057,31 @@ func (s *S) TestSignatureWithEndpointPath(c *C) {
c.Assert(req.Form["Signature"], DeepEquals, []string{"QmvgkYGn19WirCuCz/jRp3RmRgFwWR5WRkKZ5AZnyXQ="})
}
func (s *S) TestDescribeInstanceStatusExample(c *C) {
testServer.Response(200, nil, DescribeInstanceStatusExample)
options := &ec2.DescribeInstanceStatus{}
resp, err := s.ec2.DescribeInstanceStatus(options, nil)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeInstanceStatus"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "3be1508e-c444-4fef-89cc-0b1223c4f02fEXAMPLE")
c.Assert(resp.InstanceStatus[0].InstanceId, Equals, "i-1a2b3c4d")
c.Assert(resp.InstanceStatus[0].InstanceState.Code, Equals, 16)
c.Assert(resp.InstanceStatus[0].SystemStatus.Status, Equals, "impaired")
c.Assert(resp.InstanceStatus[0].SystemStatus.Details[0].Name, Equals, "reachability")
c.Assert(resp.InstanceStatus[0].SystemStatus.Details[0].Status, Equals, "failed")
c.Assert(resp.InstanceStatus[0].SystemStatus.Details[0].ImpairedSince, Equals, "YYYY-MM-DDTHH:MM:SS.000Z")
c.Assert(resp.InstanceStatus[0].InstanceStatus.Details[0].Name, Equals, "reachability")
c.Assert(resp.InstanceStatus[0].InstanceStatus.Details[0].Status, Equals, "failed")
c.Assert(resp.InstanceStatus[0].InstanceStatus.Details[0].ImpairedSince, Equals, "YYYY-MM-DDTHH:MM:SS.000Z")
c.Assert(resp.InstanceStatus[0].Events[0].Code, Equals, "instance-retirement")
c.Assert(resp.InstanceStatus[0].Events[0].Description, Equals, "The instance is running on degraded hardware")
c.Assert(resp.InstanceStatus[0].Events[0].NotBefore, Equals, "YYYY-MM-DDTHH:MM:SS+0000")
c.Assert(resp.InstanceStatus[0].Events[0].NotAfter, Equals, "YYYY-MM-DDTHH:MM:SS+0000")
}
func (s *S) TestAllocateAddressExample(c *C) {
testServer.Response(200, nil, AllocateAddressExample)
@@ -1223,6 +1264,24 @@ func (s *S) TestCreateSubnet(c *C) {
c.Assert(resp.Subnet.AvailableIpAddressCount, Equals, 251)
}
func (s *S) TestModifySubnetAttribute(c *C) {
testServer.Response(200, nil, ModifySubnetAttributeExample)
options := &ec2.ModifySubnetAttribute{
SubnetId: "foo",
MapPublicIpOnLaunch: true,
}
resp, err := s.ec2.ModifySubnetAttribute(options)
req := testServer.WaitRequest()
c.Assert(req.Form["SubnetId"], DeepEquals, []string{"foo"})
c.Assert(req.Form["MapPublicIpOnLaunch.Value"], DeepEquals, []string{"true"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "59dbff89-35bd-4eac-99ed-be587EXAMPLE")
}
func (s *S) TestResetImageAttribute(c *C) {
testServer.Response(200, nil, ResetImageAttributeExample)
@@ -108,7 +108,7 @@ var RequestSpotInstancesExample = `
<state>open</state>
<status>
<code>pending-evaluation</code>
<updateTime>YYYY-MM-DDTHH:MM:SS.000Z</updateTime>
<updateTime>2008-05-07T12:51:50.000Z</updateTime>
<message>Your Spot request has been submitted for review, and is pending evaluation.</message>
</status>
<availabilityZoneGroup>MyAzGroup</availabilityZoneGroup>
@@ -147,7 +147,7 @@ var DescribeSpotRequestsExample = `
<state>active</state>
<status>
<code>fulfilled</code>
<updateTime>YYYY-MM-DDTHH:MM:SS.000Z</updateTime>
<updateTime>2008-05-07T12:51:50.000Z</updateTime>
<message>Your Spot request is fulfilled.</message>
</status>
<launchSpecification>
@@ -766,6 +766,137 @@ var AllocateAddressExample = `
</AllocateAddressResponse>
`
// http://goo.gl/DFySJY
var DescribeInstanceStatusExample = `
<DescribeInstanceStatusResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>3be1508e-c444-4fef-89cc-0b1223c4f02fEXAMPLE</requestId>
<instanceStatusSet>
<item>
<instanceId>i-1a2b3c4d</instanceId>
<availabilityZone>us-east-1d</availabilityZone>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<systemStatus>
<status>impaired</status>
<details>
<item>
<name>reachability</name>
<status>failed</status>
<impairedSince>YYYY-MM-DDTHH:MM:SS.000Z</impairedSince>
</item>
</details>
</systemStatus>
<instanceStatus>
<status>impaired</status>
<details>
<item>
<name>reachability</name>
<status>failed</status>
<impairedSince>YYYY-MM-DDTHH:MM:SS.000Z</impairedSince>
</item>
</details>
</instanceStatus>
<eventsSet>
<item>
<code>instance-retirement</code>
<description>The instance is running on degraded hardware</description>
<notBefore>YYYY-MM-DDTHH:MM:SS+0000</notBefore>
<notAfter>YYYY-MM-DDTHH:MM:SS+0000</notAfter>
</item>
</eventsSet>
</item>
<item>
<instanceId>i-2a2b3c4d</instanceId>
<availabilityZone>us-east-1d</availabilityZone>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<systemStatus>
<status>ok</status>
<details>
<item>
<name>reachability</name>
<status>passed</status>
</item>
</details>
</systemStatus>
<instanceStatus>
<status>ok</status>
<details>
<item>
<name>reachability</name>
<status>passed</status>
</item>
</details>
</instanceStatus>
<eventsSet>
<item>
<code>instance-reboot</code>
<description>The instance is scheduled for a reboot</description>
<notBefore>YYYY-MM-DDTHH:MM:SS+0000</notBefore>
<notAfter>YYYY-MM-DDTHH:MM:SS+0000</notAfter>
</item>
</eventsSet>
</item>
<item>
<instanceId>i-3a2b3c4d</instanceId>
<availabilityZone>us-east-1c</availabilityZone>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<systemStatus>
<status>ok</status>
<details>
<item>
<name>reachability</name>
<status>passed</status>
</item>
</details>
</systemStatus>
<instanceStatus>
<status>ok</status>
<details>
<item>
<name>reachability</name>
<status>passed</status>
</item>
</details>
</instanceStatus>
</item>
<item>
<instanceId>i-4a2b3c4d</instanceId>
<availabilityZone>us-east-1c</availabilityZone>
<instanceState>
<code>16</code>
<name>running</name>
</instanceState>
<systemStatus>
<status>ok</status>
<details>
<item>
<name>reachability</name>
<status>passed</status>
</item>
</details>
</systemStatus>
<instanceStatus>
<status>insufficient-data</status>
<details>
<item>
<name>reachability</name>
<status>insufficient-data</status>
</item>
</details>
</instanceStatus>
</item>
</instanceStatusSet>
</DescribeInstanceStatusResponse>
`
// http://goo.gl/3Q0oCc
var ReleaseAddressExample = `
<ReleaseAddressResponse xmlns="http://ec2.amazonaws.com/doc/2013-10-15/">
@@ -845,6 +976,14 @@ var CreateSubnetExample = `
</CreateSubnetResponse>
`
// http://goo.gl/tu2Kxm
var ModifySubnetAttributeExample = `
<ModifySubnetAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
<return>true</return>
</ModifySubnetAttributeResponse>
`
// http://goo.gl/r6ZCPm
var ResetImageAttributeExample = `
<ResetImageAttributeResponse xmlns="http://ec2.amazonaws.com/doc/2014-06-15/">
@@ -93,21 +93,28 @@ func makeParams(action string) map[string]string {
type Listener struct {
InstancePort int64 `xml:"member>Listener>InstancePort"`
InstanceProtocol string `xml:"member>Listener>InstanceProtocol"`
SSLCertificateId string `xml:"member>Listener>SSLCertificateId"`
LoadBalancerPort int64 `xml:"member>Listener>LoadBalancerPort"`
Protocol string `xml:"member>Listener>Protocol"`
}
// An Instance attaches to an elb
type Instance struct {
InstanceId string `xml:"member>InstanceId"`
InstanceId string `xml:"InstanceId"`
}
// A tag attached to an elb
type Tag struct {
Key string `xml:"Key"`
Value string `xml:"Value"`
}
// An InstanceState from an elb health query
type InstanceState struct {
InstanceId string `xml:"member>InstanceId"`
Description string `xml:"member>Description"`
State string `xml:"member>State"`
ReasonCode string `xml:"member>ReasonCode"`
InstanceId string `xml:"InstanceId"`
Description string `xml:"Description"`
State string `xml:"State"`
ReasonCode string `xml:"ReasonCode"`
}
// An Instance attaches to an elb
@@ -115,6 +122,67 @@ type AvailabilityZone struct {
AvailabilityZone string `xml:"member"`
}
// ----------------------------------------------------------------------------
// AddTags
type AddTags struct {
LoadBalancerNames []string
Tags []Tag
}
type AddTagsResp struct {
RequestId string `xml:"ResponseMetadata>RequestId"`
}
func (elb *ELB) AddTags(options *AddTags) (resp *AddTagsResp, err error) {
params := makeParams("AddTags")
for i, v := range options.LoadBalancerNames {
params["LoadBalancerNames.member."+strconv.Itoa(i+1)] = v
}
for i, v := range options.Tags {
params["Tags.member."+strconv.Itoa(i+1)+".Key"] = v.Key
params["Tags.member."+strconv.Itoa(i+1)+".Value"] = v.Value
}
resp = &AddTagsResp{}
err = elb.query(params, resp)
return resp, err
}
// ----------------------------------------------------------------------------
// RemoveTags
type RemoveTags struct {
LoadBalancerNames []string
TagKeys []string
}
type RemoveTagsResp struct {
RequestId string `xml:"ResponseMetadata>RequestId"`
}
func (elb *ELB) RemoveTags(options *RemoveTags) (resp *RemoveTagsResp, err error) {
params := makeParams("RemoveTags")
for i, v := range options.LoadBalancerNames {
params["LoadBalancerNames.member."+strconv.Itoa(i+1)] = v
}
for i, v := range options.TagKeys {
params["Tags.member."+strconv.Itoa(i+1)+".Key"] = v
}
resp = &RemoveTagsResp{}
err = elb.query(params, resp)
return resp, err
}
// ----------------------------------------------------------------------------
// Create
@@ -126,6 +194,7 @@ type CreateLoadBalancer struct {
Internal bool // true for vpc elbs
SecurityGroups []string
Subnets []string
Tags []Tag
}
type CreateLoadBalancerResp struct {
@@ -155,6 +224,12 @@ func (elb *ELB) CreateLoadBalancer(options *CreateLoadBalancer) (resp *CreateLoa
params["Listeners.member."+strconv.Itoa(i+1)+".InstancePort"] = strconv.FormatInt(v.InstancePort, 10)
params["Listeners.member."+strconv.Itoa(i+1)+".Protocol"] = v.Protocol
params["Listeners.member."+strconv.Itoa(i+1)+".InstanceProtocol"] = v.InstanceProtocol
params["Listeners.member."+strconv.Itoa(i+1)+".SSLCertificateId"] = v.SSLCertificateId
}
for i, v := range options.Tags {
params["Tags.member."+strconv.Itoa(i+1)+".Key"] = v.Key
params["Tags.member."+strconv.Itoa(i+1)+".Value"] = v.Value
}
if options.Internal {
@@ -203,10 +278,13 @@ func (elb *ELB) DeleteLoadBalancer(options *DeleteLoadBalancer) (resp *SimpleRes
type LoadBalancer struct {
LoadBalancerName string `xml:"member>LoadBalancerName"`
Listeners []Listener `xml:"member>ListenerDescriptions"`
Instances []Instance `xml:"member>Instances"`
Instances []Instance `xml:"member>Instances>member"`
HealthCheck HealthCheck `xml:"member>HealthCheck"`
AvailabilityZones []AvailabilityZone `xml:"member>AvailabilityZones"`
Scheme string `xml:"member>Scheme"`
DNSName string `xml:"member>DNSName"`
SecurityGroups []string `xml:"member>SecurityGroups>member"`
Scheme string `xml:"member>Scheme"`
Subnets []string `xml:"member>Subnets>member"`
}
// DescribeLoadBalancer request params
@@ -302,6 +380,84 @@ func (elb *ELB) DeregisterInstancesFromLoadBalancer(options *DeregisterInstances
return
}
// ----------------------------------------------------------------------------
// DescribeTags
type DescribeTags struct {
LoadBalancerNames []string
}
type LoadBalancerTag struct {
Tags []Tag `xml:"Tags>member"`
LoadBalancerName string `xml:"LoadBalancerName"`
}
type DescribeTagsResp struct {
LoadBalancerTags []LoadBalancerTag `xml:"DescribeTagsResult>TagDescriptions>member"`
NextToken string `xml:"DescribeTagsResult>NextToken"`
RequestId string `xml:"ResponseMetadata>RequestId"`
}
func (elb *ELB) DescribeTags(options *DescribeTags) (resp *DescribeTagsResp, err error) {
params := makeParams("DescribeTags")
for i, v := range options.LoadBalancerNames {
params["LoadBalancerNames.member."+strconv.Itoa(i+1)] = v
}
resp = &DescribeTagsResp{}
err = elb.query(params, resp)
if err != nil {
resp = nil
}
return
}
// ----------------------------------------------------------------------------
// Health Checks
type HealthCheck struct {
HealthyThreshold int64 `xml:"HealthyThreshold"`
UnhealthyThreshold int64 `xml:"UnhealthyThreshold"`
Interval int64 `xml:"Interval"`
Target string `xml:"Target"`
Timeout int64 `xml:"Timeout"`
}
type ConfigureHealthCheck struct {
LoadBalancerName string
Check HealthCheck
}
type ConfigureHealthCheckResp struct {
Check HealthCheck `xml:"ConfigureHealthCheckResult>HealthCheck"`
RequestId string `xml:"ResponseMetadata>RequestId"`
}
func (elb *ELB) ConfigureHealthCheck(options *ConfigureHealthCheck) (resp *ConfigureHealthCheckResp, err error) {
params := makeParams("ConfigureHealthCheck")
params["LoadBalancerName"] = options.LoadBalancerName
params["HealthCheck.HealthyThreshold"] = strconv.Itoa(int(options.Check.HealthyThreshold))
params["HealthCheck.UnhealthyThreshold"] = strconv.Itoa(int(options.Check.UnhealthyThreshold))
params["HealthCheck.Interval"] = strconv.Itoa(int(options.Check.Interval))
params["HealthCheck.Target"] = options.Check.Target
params["HealthCheck.Timeout"] = strconv.Itoa(int(options.Check.Timeout))
resp = &ConfigureHealthCheckResp{}
err = elb.query(params, resp)
if err != nil {
resp = nil
}
return
}
// ----------------------------------------------------------------------------
// Instance Health
@@ -311,7 +467,7 @@ type DescribeInstanceHealth struct {
}
type DescribeInstanceHealthResp struct {
InstanceStates []InstanceState `xml:"DescribeInstanceHealthResult>InstanceStates"`
InstanceStates []InstanceState `xml:"DescribeInstanceHealthResult>InstanceStates>member"`
RequestId string `xml:"ResponseMetadata>RequestId"`
}
@@ -30,6 +30,48 @@ func (s *S) TearDownTest(c *C) {
testServer.Flush()
}
func (s *S) TestAddTags(c *C) {
testServer.Response(200, nil, AddTagsExample)
options := elb.AddTags{
LoadBalancerNames: []string{"foobar"},
Tags: []elb.Tag{
{
Key: "hello",
Value: "world",
},
},
}
resp, err := s.elb.AddTags(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"AddTags"})
c.Assert(req.Form["LoadBalancerNames.member.1"], DeepEquals, []string{"foobar"})
c.Assert(req.Form["Tags.member.1.Key"], DeepEquals, []string{"hello"})
c.Assert(req.Form["Tags.member.1.Value"], DeepEquals, []string{"world"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "360e81f7-1100-11e4-b6ed-0f30EXAMPLE")
}
func (s *S) TestRemoveTags(c *C) {
testServer.Response(200, nil, RemoveTagsExample)
options := elb.RemoveTags{
LoadBalancerNames: []string{"foobar"},
TagKeys: []string{"hello"},
}
resp, err := s.elb.RemoveTags(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"RemoveTags"})
c.Assert(req.Form["LoadBalancerNames.member.1"], DeepEquals, []string{"foobar"})
c.Assert(req.Form["Tags.member.1.Key"], DeepEquals, []string{"hello"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
}
func (s *S) TestCreateLoadBalancer(c *C) {
testServer.Response(200, nil, CreateLoadBalancerExample)
@@ -38,6 +80,7 @@ func (s *S) TestCreateLoadBalancer(c *C) {
Listeners: []elb.Listener{elb.Listener{
InstancePort: 80,
InstanceProtocol: "http",
SSLCertificateId: "needToAddASSLCertToYourAWSAccount",
LoadBalancerPort: 80,
Protocol: "http",
},
@@ -92,6 +135,11 @@ func (s *S) TestDescribeLoadBalancers(c *C) {
c.Assert(resp.LoadBalancers[0].AvailabilityZones[0].AvailabilityZone, Equals, "us-east-1a")
c.Assert(resp.LoadBalancers[0].Scheme, Equals, "internet-facing")
c.Assert(resp.LoadBalancers[0].DNSName, Equals, "MyLoadBalancer-123456789.us-east-1.elb.amazonaws.com")
c.Assert(resp.LoadBalancers[0].HealthCheck.HealthyThreshold, Equals, int64(2))
c.Assert(resp.LoadBalancers[0].HealthCheck.UnhealthyThreshold, Equals, int64(10))
c.Assert(resp.LoadBalancers[0].HealthCheck.Interval, Equals, int64(90))
c.Assert(resp.LoadBalancers[0].HealthCheck.Target, Equals, "HTTP:80/")
c.Assert(resp.LoadBalancers[0].HealthCheck.Timeout, Equals, int64(60))
}
func (s *S) TestRegisterInstancesWithLoadBalancer(c *C) {
@@ -136,6 +184,35 @@ func (s *S) TestDeregisterInstancesFromLoadBalancer(c *C) {
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
}
func (s *S) TestConfigureHealthCheck(c *C) {
testServer.Response(200, nil, ConfigureHealthCheckExample)
options := elb.ConfigureHealthCheck{
LoadBalancerName: "foobar",
Check: elb.HealthCheck{
HealthyThreshold: 2,
UnhealthyThreshold: 2,
Interval: 30,
Target: "HTTP:80/ping",
Timeout: 3,
},
}
resp, err := s.elb.ConfigureHealthCheck(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"ConfigureHealthCheck"})
c.Assert(req.Form["LoadBalancerName"], DeepEquals, []string{"foobar"})
c.Assert(err, IsNil)
c.Assert(resp.Check.HealthyThreshold, Equals, int64(2))
c.Assert(resp.Check.UnhealthyThreshold, Equals, int64(2))
c.Assert(resp.Check.Interval, Equals, int64(30))
c.Assert(resp.Check.Target, Equals, "HTTP:80/ping")
c.Assert(resp.Check.Timeout, Equals, int64(3))
c.Assert(resp.RequestId, Equals, "83c88b9d-12b7-11e3-8b82-87b12EXAMPLE")
}
func (s *S) TestDescribeInstanceHealth(c *C) {
testServer.Response(200, nil, DescribeInstanceHealthExample)
@@ -152,5 +229,7 @@ func (s *S) TestDescribeInstanceHealth(c *C) {
c.Assert(resp.InstanceStates[0].InstanceId, Equals, "i-90d8c2a5")
c.Assert(resp.InstanceStates[0].State, Equals, "InService")
c.Assert(resp.InstanceStates[1].InstanceId, Equals, "i-06ea3e60")
c.Assert(resp.InstanceStates[1].State, Equals, "OutOfService")
c.Assert(resp.RequestId, Equals, "1549581b-12b7-11e3-895e-1334aEXAMPLE")
}
@@ -7,6 +7,26 @@ var ErrorDump = `
</Error></Errors><RequestID>0503f4e9-bbd6-483c-b54f-c4ae9f3b30f4</RequestID></Response>
`
// http://goo.gl/OkMdtJ
var AddTagsExample = `
<AddTagsResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<AddTagsResult/>
<ResponseMetadata>
<RequestId>360e81f7-1100-11e4-b6ed-0f30EXAMPLE</RequestId>
</ResponseMetadata>
</AddTagsResponse>
`
// http://goo.gl/nT2E89
var RemoveTagsExample = `
<RemoveTagsResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<RemoveTagsResult/>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</RemoveTagsResponse>
`
// http://goo.gl/gQRD2H
var CreateLoadBalancerExample = `
<CreateLoadBalancerResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
@@ -51,6 +71,7 @@ var DescribeLoadBalancersExample = `
<Protocol>HTTP</Protocol>
<LoadBalancerPort>80</LoadBalancerPort>
<InstanceProtocol>HTTP</InstanceProtocol>
<SSLCertificateId>needToAddASSLCertToYourAWSAccount</SSLCertificateId>
<InstancePort>80</InstancePort>
</Listener>
</member>
@@ -119,6 +140,23 @@ var DeregisterInstancesFromLoadBalancerExample = `
</DeregisterInstancesFromLoadBalancerResponse>
`
// http://docs.aws.amazon.com/ElasticLoadBalancing/latest/APIReference/API_ConfigureHealthCheck.html
var ConfigureHealthCheckExample = `
<ConfigureHealthCheckResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
<ConfigureHealthCheckResult>
<HealthCheck>
<Interval>30</Interval>
<Target>HTTP:80/ping</Target>
<HealthyThreshold>2</HealthyThreshold>
<Timeout>3</Timeout>
<UnhealthyThreshold>2</UnhealthyThreshold>
</HealthCheck>
</ConfigureHealthCheckResult>
<ResponseMetadata>
<RequestId>83c88b9d-12b7-11e3-8b82-87b12EXAMPLE</RequestId>
</ResponseMetadata>
</ConfigureHealthCheckResponse>`
// http://goo.gl/cGNxfj
var DescribeInstanceHealthExample = `
<DescribeInstanceHealthResponse xmlns="http://elasticloadbalancing.amazonaws.com/doc/2012-06-01/">
@@ -130,6 +168,12 @@ var DescribeInstanceHealthExample = `
<State>InService</State>
<ReasonCode>N/A</ReasonCode>
</member>
<member>
<Description>N/A</Description>
<InstanceId>i-06ea3e60</InstanceId>
<State>OutOfService</State>
<ReasonCode>N/A</ReasonCode>
</member>
</InstanceStates>
</DescribeInstanceHealthResult>
<ResponseMetadata>
@@ -4,11 +4,12 @@ import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"github.com/mitchellh/goamz/aws"
"net/http"
"net/url"
"sort"
"strings"
"github.com/mitchellh/goamz/aws"
)
var b64 = base64.StdEncoding
@@ -31,7 +32,7 @@ func sign(auth aws.Auth, method, path string, params url.Values, headers http.He
params["SignatureVersion"] = []string{"2"}
params["SignatureMethod"] = []string{"HmacSHA256"}
if auth.Token != "" {
params["SecurityToken"] = auth.Token
params["SecurityToken"] = []string{auth.Token}
}
// join up all the incoming params
@@ -0,0 +1,51 @@
package sns
import (
"strconv"
)
type Permission struct {
ActionName string
AccountId string
}
type AddPermissionResponse struct {
ResponseMetadata
}
// AddPermission
//
// See http://goo.gl/mbY4a for more details.
func (sns *SNS) AddPermission(permissions []Permission, Label, TopicArn string) (resp *AddPermissionResponse, err error) {
resp = &AddPermissionResponse{}
params := makeParams("AddPermission")
for i, p := range permissions {
params["AWSAccountId.member."+strconv.Itoa(i+1)] = p.AccountId
params["ActionName.member."+strconv.Itoa(i+1)] = p.ActionName
}
params["Label"] = Label
params["TopicArn"] = TopicArn
err = sns.query(params, resp)
return
}
type RemovePermissionResponse struct {
ResponseMetadata
}
// RemovePermission
//
// See http://goo.gl/wGl5j for more details.
func (sns *SNS) RemovePermission(Label, TopicArn string) (resp *RemovePermissionResponse, err error) {
resp = &RemovePermissionResponse{}
params := makeParams("RemovePermission")
params["Label"] = Label
params["TopicArn"] = TopicArn
err = sns.query(params, resp)
return
}
@@ -13,24 +13,16 @@
// first-class package in goamz.
package sns
// BUG(niemeyer): Package needs significant clean up.
// BUG(niemeyer): Topic values in responses are not being initialized
// properly, since they're supposed to reference *SNS.
// BUG(niemeyer): Package needs documentation.
// BUG(niemeyer): Message.Message should be "Payload []byte"
// BUG(niemeyer): Message.SNS must be dropped.
import (
"encoding/xml"
"errors"
"github.com/mitchellh/goamz/aws"
"net/http"
"net/url"
"strconv"
"time"
)
@@ -41,32 +33,9 @@ type SNS struct {
private byte // Reserve the right of using private data.
}
type Topic struct {
SNS *SNS
TopicArn string
}
func New(auth aws.Auth, region aws.Region) *SNS {
return &SNS{auth, region, 0}
}
type Message struct {
SNS *SNS
Topic *Topic
Message [8192]byte
Subject string
}
type Subscription struct {
Endpoint string
Owner string
Protocol string
SubscriptionArn string
TopicArn string
}
func (topic *Topic) Message(message [8192]byte, subject string) *Message {
return &Message{topic.SNS, topic, message, subject}
type AttributeEntry struct {
Key string `xml:"key"`
Value string `xml:"value"`
}
type ResponseMetadata struct {
@@ -74,35 +43,8 @@ type ResponseMetadata struct {
BoxUsage float64 `xml:"ResponseMetadata>BoxUsage"`
}
type ListTopicsResp struct {
Topics []Topic `xml:"ListTopicsResult>Topics>member"`
NextToken string
ResponseMetadata
}
type CreateTopicResp struct {
Topic Topic `xml:"CreateTopicResult"`
ResponseMetadata
}
type DeleteTopicResp struct {
ResponseMetadata
}
type ListSubscriptionsResp struct {
Subscriptions []Subscription `xml:"ListSubscriptionsResult>Subscriptions>member"`
NextToken string
ResponseMetadata
}
type AttributeEntry struct {
Key string `xml:"key"`
Value string `xml:"value"`
}
type GetTopicAttributesResp struct {
Attributes []AttributeEntry `xml:"GetTopicAttributesResult>Attributes>entry"`
ResponseMetadata
func New(auth aws.Auth, region aws.Region) *SNS {
return &SNS{auth, region, 0}
}
func makeParams(action string) map[string]string {
@@ -111,273 +53,6 @@ func makeParams(action string) map[string]string {
return params
}
// ListTopics
//
// See http://goo.gl/lfrMK for more details.
func (sns *SNS) ListTopics(NextToken *string) (resp *ListTopicsResp, err error) {
resp = &ListTopicsResp{}
params := makeParams("ListTopics")
if NextToken != nil {
params["NextToken"] = *NextToken
}
err = sns.query(nil, nil, params, resp)
return
}
// CreateTopic
//
// See http://goo.gl/m9aAt for more details.
func (sns *SNS) CreateTopic(Name string) (resp *CreateTopicResp, err error) {
resp = &CreateTopicResp{}
params := makeParams("CreateTopic")
params["Name"] = Name
err = sns.query(nil, nil, params, resp)
return
}
// DeleteTopic
//
// See http://goo.gl/OXNcY for more details.
func (sns *SNS) DeleteTopic(topic Topic) (resp *DeleteTopicResp, err error) {
resp = &DeleteTopicResp{}
params := makeParams("DeleteTopic")
params["TopicArn"] = topic.TopicArn
err = sns.query(nil, nil, params, resp)
return
}
// Delete
//
// Helper function for deleting a topic
func (topic *Topic) Delete() (resp *DeleteTopicResp, err error) {
return topic.SNS.DeleteTopic(*topic)
}
// ListSubscriptions
//
// See http://goo.gl/k3aGn for more details.
func (sns *SNS) ListSubscriptions(NextToken *string) (resp *ListSubscriptionsResp, err error) {
resp = &ListSubscriptionsResp{}
params := makeParams("ListSubscriptions")
if NextToken != nil {
params["NextToken"] = *NextToken
}
err = sns.query(nil, nil, params, resp)
return
}
// GetTopicAttributes
//
// See http://goo.gl/WXRoX for more details.
func (sns *SNS) GetTopicAttributes(TopicArn string) (resp *GetTopicAttributesResp, err error) {
resp = &GetTopicAttributesResp{}
params := makeParams("GetTopicAttributes")
params["TopicArn"] = TopicArn
err = sns.query(nil, nil, params, resp)
return
}
type PublishOpt struct {
Message string
MessageStructure string
Subject string
TopicArn string
}
type PublishResp struct {
MessageId string `xml:"PublishResult>MessageId"`
ResponseMetadata
}
// Publish
//
// See http://goo.gl/AY2D8 for more details.
func (sns *SNS) Publish(options *PublishOpt) (resp *PublishResp, err error) {
resp = &PublishResp{}
params := makeParams("Publish")
if options.Subject != "" {
params["Subject"] = options.Subject
}
if options.MessageStructure != "" {
params["MessageStructure"] = options.MessageStructure
}
if options.Message != "" {
params["Message"] = options.Message
}
if options.TopicArn != "" {
params["TopicArn"] = options.TopicArn
}
err = sns.query(nil, nil, params, resp)
return
}
type SetTopicAttributesResponse struct {
ResponseMetadata
}
// SetTopicAttributes
//
// See http://goo.gl/oVYW7 for more details.
func (sns *SNS) SetTopicAttributes(AttributeName, AttributeValue, TopicArn string) (resp *SetTopicAttributesResponse, err error) {
resp = &SetTopicAttributesResponse{}
params := makeParams("SetTopicAttributes")
if AttributeName == "" || TopicArn == "" {
return nil, errors.New("Invalid Attribute Name or TopicArn")
}
params["AttributeName"] = AttributeName
params["AttributeValue"] = AttributeValue
params["TopicArn"] = TopicArn
err = sns.query(nil, nil, params, resp)
return
}
type SubscribeResponse struct {
SubscriptionArn string `xml:"SubscribeResult>SubscriptionArn"`
ResponseMetadata
}
// Subscribe
//
// See http://goo.gl/c3iGS for more details.
func (sns *SNS) Subscribe(Endpoint, Protocol, TopicArn string) (resp *SubscribeResponse, err error) {
resp = &SubscribeResponse{}
params := makeParams("Subscribe")
params["Endpoint"] = Endpoint
params["Protocol"] = Protocol
params["TopicArn"] = TopicArn
err = sns.query(nil, nil, params, resp)
return
}
type UnsubscribeResponse struct {
ResponseMetadata
}
// Unsubscribe
//
// See http://goo.gl/4l5Ge for more details.
func (sns *SNS) Unsubscribe(SubscriptionArn string) (resp *UnsubscribeResponse, err error) {
resp = &UnsubscribeResponse{}
params := makeParams("Unsubscribe")
params["SubscriptionArn"] = SubscriptionArn
err = sns.query(nil, nil, params, resp)
return
}
type ConfirmSubscriptionResponse struct {
SubscriptionArn string `xml:"ConfirmSubscriptionResult>SubscriptionArn"`
ResponseMetadata
}
type ConfirmSubscriptionOpt struct {
AuthenticateOnUnsubscribe string
Token string
TopicArn string
}
// ConfirmSubscription
//
// See http://goo.gl/3hXzH for more details.
func (sns *SNS) ConfirmSubscription(options *ConfirmSubscriptionOpt) (resp *ConfirmSubscriptionResponse, err error) {
resp = &ConfirmSubscriptionResponse{}
params := makeParams("ConfirmSubscription")
if options.AuthenticateOnUnsubscribe != "" {
params["AuthenticateOnUnsubscribe"] = options.AuthenticateOnUnsubscribe
}
params["Token"] = options.Token
params["TopicArn"] = options.TopicArn
err = sns.query(nil, nil, params, resp)
return
}
type Permission struct {
ActionName string
AccountId string
}
type AddPermissionResponse struct {
ResponseMetadata
}
// AddPermission
//
// See http://goo.gl/mbY4a for more details.
func (sns *SNS) AddPermission(permissions []Permission, Label, TopicArn string) (resp *AddPermissionResponse, err error) {
resp = &AddPermissionResponse{}
params := makeParams("AddPermission")
for i, p := range permissions {
params["AWSAccountId.member."+strconv.Itoa(i+1)] = p.AccountId
params["ActionName.member."+strconv.Itoa(i+1)] = p.ActionName
}
params["Label"] = Label
params["TopicArn"] = TopicArn
err = sns.query(nil, nil, params, resp)
return
}
type RemovePermissionResponse struct {
ResponseMetadata
}
// RemovePermission
//
// See http://goo.gl/wGl5j for more details.
func (sns *SNS) RemovePermission(Label, TopicArn string) (resp *RemovePermissionResponse, err error) {
resp = &RemovePermissionResponse{}
params := makeParams("RemovePermission")
params["Label"] = Label
params["TopicArn"] = TopicArn
err = sns.query(nil, nil, params, resp)
return
}
type ListSubscriptionByTopicResponse struct {
Subscriptions []Subscription `xml:"ListSubscriptionsByTopicResult>Subscriptions>member"`
ResponseMetadata
}
type ListSubscriptionByTopicOpt struct {
NextToken string
TopicArn string
}
// ListSubscriptionByTopic
//
// See http://goo.gl/LaVcC for more details.
func (sns *SNS) ListSubscriptionByTopic(options *ListSubscriptionByTopicOpt) (resp *ListSubscriptionByTopicResponse, err error) {
resp = &ListSubscriptionByTopicResponse{}
params := makeParams("ListSbubscriptionByTopic")
if options.NextToken != "" {
params["NextToken"] = options.NextToken
}
params["TopicArn"] = options.TopicArn
err = sns.query(nil, nil, params, resp)
return
}
type Error struct {
StatusCode int
Code string
@@ -394,7 +69,7 @@ type xmlErrors struct {
Errors []Error `xml:"Errors>Error"`
}
func (sns *SNS) query(topic *Topic, message *Message, params map[string]string, resp interface{}) error {
func (sns *SNS) query(params map[string]string, resp interface{}) error {
params["Timestamp"] = time.Now().UTC().Format(time.RFC3339)
u, err := url.Parse(sns.Region.SNSEndpoint)
if err != nil {
@@ -409,10 +84,6 @@ func (sns *SNS) query(topic *Topic, message *Message, params map[string]string,
}
defer r.Body.Close()
//dump, _ := http.DumpResponse(r, true)
//println("DUMP:\n", string(dump))
//return nil
if r.StatusCode != 200 {
return buildError(r)
}
@@ -40,6 +40,7 @@ func (s *S) TestListTopicsOK(c *C) {
c.Assert(req.URL.Path, Equals, "/")
c.Assert(req.Header["Date"], Not(Equals), "")
c.Assert(resp.Topics[0].SNS, Equals, s.sns)
c.Assert(resp.ResponseMetadata.RequestId, Equals, "bd10b26c-e30e-11e0-ba29-93c3aca2f103")
c.Assert(err, IsNil)
}
@@ -54,6 +55,7 @@ func (s *S) TestCreateTopic(c *C) {
c.Assert(req.URL.Path, Equals, "/")
c.Assert(req.Header["Date"], Not(Equals), "")
c.Assert(resp.Topic.SNS, Equals, s.sns)
c.Assert(resp.Topic.TopicArn, Equals, "arn:aws:sns:us-east-1:123456789012:My-Topic")
c.Assert(resp.ResponseMetadata.RequestId, Equals, "a8dec8b3-33a4-11df-8963-01868b7c937a")
c.Assert(err, IsNil)
@@ -62,8 +64,8 @@ func (s *S) TestCreateTopic(c *C) {
func (s *S) TestDeleteTopic(c *C) {
testServer.Response(200, nil, TestDeleteTopicXmlOK)
t := sns.Topic{nil, "arn:aws:sns:us-east-1:123456789012:My-Topic"}
resp, err := s.sns.DeleteTopic(t)
t := sns.Topic{s.sns, "arn:aws:sns:us-east-1:123456789012:My-Topic"}
resp, err := t.Delete()
req := testServer.WaitRequest()
c.Assert(req.Method, Equals, "GET")
@@ -115,7 +117,14 @@ func (s *S) TestGetTopicAttributes(c *C) {
func (s *S) TestPublish(c *C) {
testServer.Response(200, nil, TestPublishXmlOK)
pubOpt := &sns.PublishOpt{"foobar", "", "subject", "arn:aws:sns:us-east-1:123456789012:My-Topic"}
pubOpt := &sns.PublishOpt{
Message: "foobar",
MessageStructure: "",
Subject: "subject",
TopicArn: "arn:aws:sns:us-east-1:123456789012:My-Topic",
TargetArn: "arn:aws:sns:us-east-1:123456789012:My-Other-Topic",
}
resp, err := s.sns.Publish(pubOpt)
req := testServer.WaitRequest()
@@ -0,0 +1,161 @@
package sns
type Subscription struct {
Endpoint string
Owner string
Protocol string
SubscriptionArn string
TopicArn string
}
type ListSubscriptionsResp struct {
Subscriptions []Subscription `xml:"ListSubscriptionsResult>Subscriptions>member"`
NextToken string
ResponseMetadata
}
type PublishOpt struct {
Message string
MessageStructure string
Subject string
TopicArn string
TargetArn string
}
type PublishResp struct {
MessageId string `xml:"PublishResult>MessageId"`
ResponseMetadata
}
type SubscribeResponse struct {
SubscriptionArn string `xml:"SubscribeResult>SubscriptionArn"`
ResponseMetadata
}
type UnsubscribeResponse struct {
ResponseMetadata
}
type ConfirmSubscriptionResponse struct {
SubscriptionArn string `xml:"ConfirmSubscriptionResult>SubscriptionArn"`
ResponseMetadata
}
type ConfirmSubscriptionOpt struct {
AuthenticateOnUnsubscribe string
Token string
TopicArn string
}
type ListSubscriptionByTopicResponse struct {
Subscriptions []Subscription `xml:"ListSubscriptionsByTopicResult>Subscriptions>member"`
ResponseMetadata
}
type ListSubscriptionByTopicOpt struct {
NextToken string
TopicArn string
}
// Publish
//
// See http://goo.gl/AY2D8 for more details.
func (sns *SNS) Publish(options *PublishOpt) (resp *PublishResp, err error) {
resp = &PublishResp{}
params := makeParams("Publish")
if options.Subject != "" {
params["Subject"] = options.Subject
}
if options.MessageStructure != "" {
params["MessageStructure"] = options.MessageStructure
}
if options.Message != "" {
params["Message"] = options.Message
}
if options.TopicArn != "" {
params["TopicArn"] = options.TopicArn
}
err = sns.query(params, resp)
return
}
// Subscribe
//
// See http://goo.gl/c3iGS for more details.
func (sns *SNS) Subscribe(Endpoint, Protocol, TopicArn string) (resp *SubscribeResponse, err error) {
resp = &SubscribeResponse{}
params := makeParams("Subscribe")
params["Endpoint"] = Endpoint
params["Protocol"] = Protocol
params["TopicArn"] = TopicArn
err = sns.query(params, resp)
return
}
// Unsubscribe
//
// See http://goo.gl/4l5Ge for more details.
func (sns *SNS) Unsubscribe(SubscriptionArn string) (resp *UnsubscribeResponse, err error) {
resp = &UnsubscribeResponse{}
params := makeParams("Unsubscribe")
params["SubscriptionArn"] = SubscriptionArn
err = sns.query(params, resp)
return
}
// ConfirmSubscription
//
// See http://goo.gl/3hXzH for more details.
func (sns *SNS) ConfirmSubscription(options *ConfirmSubscriptionOpt) (resp *ConfirmSubscriptionResponse, err error) {
resp = &ConfirmSubscriptionResponse{}
params := makeParams("ConfirmSubscription")
if options.AuthenticateOnUnsubscribe != "" {
params["AuthenticateOnUnsubscribe"] = options.AuthenticateOnUnsubscribe
}
params["Token"] = options.Token
params["TopicArn"] = options.TopicArn
err = sns.query(params, resp)
return
}
// ListSubscriptions
//
// See http://goo.gl/k3aGn for more details.
func (sns *SNS) ListSubscriptions(NextToken *string) (resp *ListSubscriptionsResp, err error) {
resp = &ListSubscriptionsResp{}
params := makeParams("ListSubscriptions")
if NextToken != nil {
params["NextToken"] = *NextToken
}
err = sns.query(params, resp)
return
}
// ListSubscriptionByTopic
//
// See http://goo.gl/LaVcC for more details.
func (sns *SNS) ListSubscriptionByTopic(options *ListSubscriptionByTopicOpt) (resp *ListSubscriptionByTopicResponse, err error) {
resp = &ListSubscriptionByTopicResponse{}
params := makeParams("ListSbubscriptionByTopic")
if options.NextToken != "" {
params["NextToken"] = options.NextToken
}
params["TopicArn"] = options.TopicArn
err = sns.query(params, resp)
return
}
@@ -0,0 +1,103 @@
package sns
import (
"errors"
)
type Topic struct {
SNS *SNS
TopicArn string
}
type ListTopicsResp struct {
Topics []Topic `xml:"ListTopicsResult>Topics>member"`
NextToken string
ResponseMetadata
}
type CreateTopicResp struct {
Topic Topic `xml:"CreateTopicResult"`
ResponseMetadata
}
type DeleteTopicResp struct {
ResponseMetadata
}
type GetTopicAttributesResp struct {
Attributes []AttributeEntry `xml:"GetTopicAttributesResult>Attributes>entry"`
ResponseMetadata
}
type SetTopicAttributesResponse struct {
ResponseMetadata
}
// ListTopics
//
// See http://goo.gl/lfrMK for more details.
func (sns *SNS) ListTopics(NextToken *string) (resp *ListTopicsResp, err error) {
resp = &ListTopicsResp{}
params := makeParams("ListTopics")
if NextToken != nil {
params["NextToken"] = *NextToken
}
err = sns.query(params, resp)
for i, _ := range resp.Topics {
resp.Topics[i].SNS = sns
}
return
}
// CreateTopic
//
// See http://goo.gl/m9aAt for more details.
func (sns *SNS) CreateTopic(Name string) (resp *CreateTopicResp, err error) {
resp = &CreateTopicResp{}
params := makeParams("CreateTopic")
params["Name"] = Name
err = sns.query(params, resp)
resp.Topic.SNS = sns
return
}
// Delete
//
// Helper function for deleting a topic
func (topic *Topic) Delete() (resp *DeleteTopicResp, err error) {
resp = &DeleteTopicResp{}
params := makeParams("DeleteTopic")
params["TopicArn"] = topic.TopicArn
err = topic.SNS.query(params, resp)
return
}
// GetTopicAttributes
//
// See http://goo.gl/WXRoX for more details.
func (sns *SNS) GetTopicAttributes(TopicArn string) (resp *GetTopicAttributesResp, err error) {
resp = &GetTopicAttributesResp{}
params := makeParams("GetTopicAttributes")
params["TopicArn"] = TopicArn
err = sns.query(params, resp)
return
}
// SetTopicAttributes
//
// See http://goo.gl/oVYW7 for more details.
func (sns *SNS) SetTopicAttributes(AttributeName, AttributeValue, TopicArn string) (resp *SetTopicAttributesResponse, err error) {
resp = &SetTopicAttributesResponse{}
params := makeParams("SetTopicAttributes")
if AttributeName == "" || TopicArn == "" {
return nil, errors.New("Invalid Attribute Name or TopicArn")
}
params["AttributeName"] = AttributeName
params["AttributeValue"] = AttributeValue
params["TopicArn"] = TopicArn
err = sns.query(params, resp)
return
}
@@ -90,23 +90,24 @@ func makeParams(action string) map[string]string {
// Rds objects
type DBInstance struct {
Address string `xml:"Endpoint>Address"`
AllocatedStorage int `xml:"AllocatedStorage"`
AvailabilityZone string `xml:"AvailabilityZone"`
BackupRetentionPeriod int `xml:"BackupRetentionPeriod"`
DBInstanceClass string `xml:"DBInstanceClass"`
DBInstanceIdentifier string `xml:"DBInstanceIdentifier"`
DBInstanceStatus string `xml:"DBInstanceStatus"`
DBName string `xml:"DBName"`
Engine string `xml:"Engine"`
EngineVersion string `xml:"EngineVersion"`
MasterUsername string `xml:"MasterUsername"`
MultiAZ bool `xml:"MultiAZ"`
Port int `xml:"Endpoint>Port"`
PreferredBackupWindow string `xml:"PreferredBackupWindow"`
PreferredMaintenanceWindow string `xml:"PreferredMaintenanceWindow"`
VpcSecurityGroupIds []string `xml:"VpcSecurityGroups"`
DBSecurityGroupNames []string `xml:"DBSecurityGroups>DBSecurityGroup>DBSecurityGroupName"`
Address string `xml:"Endpoint>Address"`
AllocatedStorage int `xml:"AllocatedStorage"`
AvailabilityZone string `xml:"AvailabilityZone"`
BackupRetentionPeriod int `xml:"BackupRetentionPeriod"`
DBInstanceClass string `xml:"DBInstanceClass"`
DBInstanceIdentifier string `xml:"DBInstanceIdentifier"`
DBInstanceStatus string `xml:"DBInstanceStatus"`
DBName string `xml:"DBName"`
Engine string `xml:"Engine"`
EngineVersion string `xml:"EngineVersion"`
MasterUsername string `xml:"MasterUsername"`
MultiAZ bool `xml:"MultiAZ"`
Port int `xml:"Endpoint>Port"`
PreferredBackupWindow string `xml:"PreferredBackupWindow"`
PreferredMaintenanceWindow string `xml:"PreferredMaintenanceWindow"`
VpcSecurityGroupIds []string `xml:"VpcSecurityGroups"`
DBSecurityGroupNames []string `xml:"DBSecurityGroups>DBSecurityGroup>DBSecurityGroupName"`
DBSubnetGroup DBSubnetGroup `xml:"DBSubnetGroup"`
}
type DBSecurityGroup struct {
@@ -119,6 +120,35 @@ type DBSecurityGroup struct {
CidrStatuses []string `xml:"IPRanges>IPRange>Status"`
}
type DBSubnetGroup struct {
Description string `xml:"DBSubnetGroupDescription"`
Name string `xml:"DBSubnetGroupName"`
Status string `xml:"SubnetGroupStatus"`
SubnetIds []string `xml:"Subnets>Subnet>SubnetIdentifier"`
VpcId string `xml:"VpcId"`
}
type DBSnapshot struct {
AllocatedStorage int `xml:"AllocatedStorage"`
AvailabilityZone string `xml:"AvailabilityZone"`
DBInstanceIdentifier string `xml:"DBInstanceIdentifier"`
DBSnapshotIdentifier string `xml:"DBSnapshotIdentifier"`
Engine string `xml:"Engine"`
EngineVersion string `xml:"EngineVersion"`
InstanceCreateTime string `xml:"InstanceCreateTime"`
Iops int `xml:"Iops"`
LicenseModel string `xml:"LicenseModel"`
MasterUsername string `xml:"MasterUsername"`
OptionGroupName string `xml:"OptionGroupName"`
PercentProgress int `xml:"PercentProgress"`
Port int `xml:"Port"`
SnapshotCreateTime string `xml:"SnapshotCreateTime"`
SnapshotType string `xml:"SnapshotType"`
SourceRegion string `xml:"SourceRegion"`
Status string `xml:"Status"`
VpcId string `xml:"VpcId"`
}
// ----------------------------------------------------------------------------
// Create
@@ -194,7 +224,7 @@ func (rds *Rds) CreateDBInstance(options *CreateDBInstance) (resp *SimpleResp, e
}
if options.EngineVersion != "" {
params["Engine"] = options.EngineVersion
params["EngineVersion"] = options.EngineVersion
}
if options.MasterUsername != "" {
@@ -263,6 +293,34 @@ func (rds *Rds) CreateDBSecurityGroup(options *CreateDBSecurityGroup) (resp *Sim
return
}
// The CreateDBSubnetGroup request parameters
type CreateDBSubnetGroup struct {
DBSubnetGroupName string
DBSubnetGroupDescription string
SubnetIds []string
}
func (rds *Rds) CreateDBSubnetGroup(options *CreateDBSubnetGroup) (resp *SimpleResp, err error) {
params := makeParams("CreateDBSubnetGroup")
params["DBSubnetGroupName"] = options.DBSubnetGroupName
params["DBSubnetGroupDescription"] = options.DBSubnetGroupDescription
for j, group := range options.SubnetIds {
params["SubnetIds.member."+strconv.Itoa(j+1)] = group
}
resp = &SimpleResp{}
err = rds.query(params, resp)
if err != nil {
resp = nil
}
return
}
// The CreateDBSecurityGroup request parameters
type AuthorizeDBSecurityGroupIngress struct {
Cidr string
@@ -358,10 +416,75 @@ func (rds *Rds) DescribeDBSecurityGroups(options *DescribeDBSecurityGroups) (res
return
}
// DescribeDBSubnetGroups request params
type DescribeDBSubnetGroups struct {
DBSubnetGroupName string
}
type DescribeDBSubnetGroupsResp struct {
RequestId string `xml:"ResponseMetadata>RequestId"`
DBSubnetGroups []DBSubnetGroup `xml:"DescribeDBSubnetGroupsResult>DBSubnetGroups>DBSubnetGroup"`
}
func (rds *Rds) DescribeDBSubnetGroups(options *DescribeDBSubnetGroups) (resp *DescribeDBSubnetGroupsResp, err error) {
params := makeParams("DescribeDBSubnetGroups")
params["DBSubnetGroupName"] = options.DBSubnetGroupName
resp = &DescribeDBSubnetGroupsResp{}
err = rds.query(params, resp)
if err != nil {
resp = nil
}
return
}
// DescribeDBSnapshots request params
type DescribeDBSnapshots struct {
DBInstanceIdentifier string
DBSnapshotIdentifier string
SnapshotType string
}
type DescribeDBSnapshotsResp struct {
RequestId string `xml:"ResponseMetadata>RequestId"`
DBSnapshots []DBSnapshot `xml:"DescribeDBSnapshotsResult>DBSnapshots>DBSnapshot"`
}
func (rds *Rds) DescribeDBSnapshots(options *DescribeDBSnapshots) (resp *DescribeDBSnapshotsResp, err error) {
params := makeParams("DescribeDBSnapshots")
if options.DBInstanceIdentifier != "" {
params["DBInstanceIdentifier"] = options.DBInstanceIdentifier
}
if options.DBSnapshotIdentifier != "" {
params["DBSnapshotIdentifier"] = options.DBSnapshotIdentifier
}
if options.SnapshotType != "" {
params["SnapshotType"] = options.SnapshotType
}
resp = &DescribeDBSnapshotsResp{}
err = rds.query(params, resp)
if err != nil {
resp = nil
}
return
}
// DeleteDBInstance request params
type DeleteDBInstance struct {
DBInstanceIdentifier string
SkipFinalSnapshot bool
FinalDBSnapshotIdentifier string
DBInstanceIdentifier string
SkipFinalSnapshot bool
}
func (rds *Rds) DeleteDBInstance(options *DeleteDBInstance) (resp *SimpleResp, err error) {
@@ -369,8 +492,12 @@ func (rds *Rds) DeleteDBInstance(options *DeleteDBInstance) (resp *SimpleResp, e
params["DBInstanceIdentifier"] = options.DBInstanceIdentifier
// If we don't skip the final snapshot, we need to specify a final
// snapshot identifier
if options.SkipFinalSnapshot {
params["SkipFinalSnapshot"] = "true"
} else {
params["FinalDBSnapshotIdentifier"] = options.FinalDBSnapshotIdentifier
}
resp = &SimpleResp{}
@@ -405,6 +532,112 @@ func (rds *Rds) DeleteDBSecurityGroup(options *DeleteDBSecurityGroup) (resp *Sim
return
}
// DeleteDBSubnetGroup request params
type DeleteDBSubnetGroup struct {
DBSubnetGroupName string
}
func (rds *Rds) DeleteDBSubnetGroup(options *DeleteDBSubnetGroup) (resp *SimpleResp, err error) {
params := makeParams("DeleteDBSubnetGroup")
params["DBSubnetGroupName"] = options.DBSubnetGroupName
resp = &SimpleResp{}
err = rds.query(params, resp)
if err != nil {
resp = nil
}
return
}
type RestoreDBInstanceFromDBSnapshot struct {
DBInstanceIdentifier string
DBSnapshotIdentifier string
AutoMinorVersionUpgrade bool
AvailabilityZone string
DBInstanceClass string
DBName string
DBSubnetGroupName string
Engine string
Iops int
LicenseModel string
MultiAZ bool
OptionGroupName string
Port int
PubliclyAccessible bool
SetIops bool
SetPort bool
}
func (rds *Rds) RestoreDBInstanceFromDBSnapshot(options *RestoreDBInstanceFromDBSnapshot) (resp *SimpleResp, err error) {
params := makeParams("RestoreDBInstanceFromDBSnapshot")
params["DBInstanceIdentifier"] = options.DBInstanceIdentifier
params["DBSnapshotIdentifier"] = options.DBSnapshotIdentifier
if options.AutoMinorVersionUpgrade {
params["AutoMinorVersionUpgrade"] = "true"
}
if options.AvailabilityZone != "" {
params["AvailabilityZone"] = options.AvailabilityZone
}
if options.DBInstanceClass != "" {
params["DBInstanceClass"] = options.DBInstanceClass
}
if options.DBName != "" {
params["DBName"] = options.DBName
}
if options.DBSubnetGroupName != "" {
params["DBSubnetGroupName"] = options.DBSubnetGroupName
}
if options.Engine != "" {
params["Engine"] = options.Engine
}
if options.SetIops {
params["Iops"] = strconv.Itoa(options.Iops)
}
if options.LicenseModel != "" {
params["LicenseModel"] = options.LicenseModel
}
if options.MultiAZ {
params["MultiAZ"] = "true"
}
if options.OptionGroupName != "" {
params["OptionGroupName"] = options.OptionGroupName
}
if options.SetPort {
params["Port"] = strconv.Itoa(options.Port)
}
if options.PubliclyAccessible {
params["PubliclyAccessible"] = "true"
}
resp = &SimpleResp{}
err = rds.query(params, resp)
if err != nil {
resp = nil
}
return
}
// Responses
type SimpleResp struct {
@@ -80,6 +80,27 @@ func (s *S) Test_CreateDBSecurityGroup(c *C) {
c.Assert(resp.RequestId, Equals, "e68ef6fa-afc1-11c3-845a-476777009d19")
}
func (s *S) Test_CreateDBSubnetGroup(c *C) {
testServer.Response(200, nil, CreateDBSubnetGroupExample)
options := rds.CreateDBSubnetGroup{
DBSubnetGroupName: "foobarbaz",
DBSubnetGroupDescription: "test description",
SubnetIds: []string{"subnet-e4d398a1", "subnet-c2bdb6ba"},
}
resp, err := s.rds.CreateDBSubnetGroup(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"CreateDBSubnetGroup"})
c.Assert(req.Form["DBSubnetGroupName"], DeepEquals, []string{"foobarbaz"})
c.Assert(req.Form["DBSubnetGroupDescription"], DeepEquals, []string{"test description"})
c.Assert(req.Form["SubnetIds.member.1"], DeepEquals, []string{"subnet-e4d398a1"})
c.Assert(req.Form["SubnetIds.member.2"], DeepEquals, []string{"subnet-c2bdb6ba"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "3a401b3f-bb9e-11d3-f4c6-37db295f7674")
}
func (s *S) Test_DescribeDBInstances(c *C) {
testServer.Response(200, nil, DescribeDBInstancesExample)
@@ -119,6 +140,25 @@ func (s *S) Test_DescribeDBSecurityGroups(c *C) {
c.Assert(resp.DBSecurityGroups[0].CidrStatuses, DeepEquals, []string{"authorized", "authorized", "authorized", "authorized"})
}
func (s *S) Test_DescribeDBSubnetGroups(c *C) {
testServer.Response(200, nil, DescribeDBSubnetGroupsExample)
options := rds.DescribeDBSubnetGroups{
DBSubnetGroupName: "foobarbaz",
}
resp, err := s.rds.DescribeDBSubnetGroups(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeDBSubnetGroups"})
c.Assert(req.Form["DBSubnetGroupName"], DeepEquals, []string{"foobarbaz"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "b783db3b-b98c-11d3-fbc7-5c0aad74da7c")
c.Assert(resp.DBSubnetGroups[0].Status, DeepEquals, "Complete")
c.Assert(resp.DBSubnetGroups[0].SubnetIds, DeepEquals, []string{"subnet-e8b3e5b1", "subnet-44b2f22e"})
c.Assert(resp.DBSubnetGroups[0].VpcId, DeepEquals, "vpc-e7abbdce")
}
func (s *S) Test_DeleteDBInstance(c *C) {
testServer.Response(200, nil, DeleteDBInstanceExample)
@@ -137,6 +177,26 @@ func (s *S) Test_DeleteDBInstance(c *C) {
c.Assert(resp.RequestId, Equals, "7369556f-b70d-11c3-faca-6ba18376ea1b")
}
func (s *S) Test_DeleteDBInstance_SnapshotIdentifier(c *C) {
testServer.Response(200, nil, DeleteDBInstanceExample)
options := rds.DeleteDBInstance{
DBInstanceIdentifier: "foobarbaz",
SkipFinalSnapshot: false,
FinalDBSnapshotIdentifier: "bar",
}
resp, err := s.rds.DeleteDBInstance(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteDBInstance"})
c.Assert(req.Form["DBInstanceIdentifier"], DeepEquals, []string{"foobarbaz"})
c.Assert(req.Form["FinalDBSnapshotIdentifier"], DeepEquals, []string{"bar"})
c.Assert(req.Form["SkipFinalSnapshot"], IsNil)
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "7369556f-b70d-11c3-faca-6ba18376ea1b")
}
func (s *S) Test_DeleteDBSecurityGroup(c *C) {
testServer.Response(200, nil, DeleteDBSecurityGroupExample)
@@ -153,6 +213,22 @@ func (s *S) Test_DeleteDBSecurityGroup(c *C) {
c.Assert(resp.RequestId, Equals, "7aec7454-ba25-11d3-855b-576787000e19")
}
func (s *S) Test_DeleteDBSubnetGroup(c *C) {
testServer.Response(200, nil, DeleteDBSubnetGroupExample)
options := rds.DeleteDBSubnetGroup{
DBSubnetGroupName: "foobarbaz",
}
resp, err := s.rds.DeleteDBSubnetGroup(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DeleteDBSubnetGroup"})
c.Assert(req.Form["DBSubnetGroupName"], DeepEquals, []string{"foobarbaz"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "6295e5ab-bbf3-11d3-f4c6-37db295f7674")
}
func (s *S) Test_AuthorizeDBSecurityGroupIngress(c *C) {
testServer.Response(200, nil, AuthorizeDBSecurityGroupIngressExample)
@@ -170,3 +246,44 @@ func (s *S) Test_AuthorizeDBSecurityGroupIngress(c *C) {
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "6176b5f8-bfed-11d3-f92b-31fa5e8dbc99")
}
func (s *S) Test_DescribeDBSnapshots(c *C) {
testServer.Response(200, nil, DescribeDBSnapshotsExample)
options := rds.DescribeDBSnapshots{
DBInstanceIdentifier: "foobar",
DBSnapshotIdentifier: "baz",
SnapshotType: "manual",
}
resp, err := s.rds.DescribeDBSnapshots(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"DescribeDBSnapshots"})
c.Assert(req.Form["DBInstanceIdentifier"], DeepEquals, []string{"foobar"})
c.Assert(req.Form["DBSnapshotIdentifier"], DeepEquals, []string{"baz"})
c.Assert(req.Form["SnapshotType"], DeepEquals, []string{"manual"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "b7769930-b98c-11d3-f272-7cd6cce12cc5")
c.Assert(resp.DBSnapshots[0].OptionGroupName, Equals, "default:mysql-5-6")
c.Assert(resp.DBSnapshots[0].Engine, Equals, "mysql")
c.Assert(resp.DBSnapshots[0].SnapshotType, Equals, "manual")
}
func (s *S) Test_RestoreDBInstanceFromDBSnapshot(c *C) {
testServer.Response(200, nil, RestoreDBInstanceFromDBSnapshotExample)
options := rds.RestoreDBInstanceFromDBSnapshot{
DBInstanceIdentifier: "foo",
DBSnapshotIdentifier: "bar",
}
resp, err := s.rds.RestoreDBInstanceFromDBSnapshot(&options)
req := testServer.WaitRequest()
c.Assert(req.Form["Action"], DeepEquals, []string{"RestoreDBInstanceFromDBSnapshot"})
c.Assert(req.Form["DBInstanceIdentifier"], DeepEquals, []string{"foo"})
c.Assert(req.Form["DBSnapshotIdentifier"], DeepEquals, []string{"bar"})
c.Assert(err, IsNil)
c.Assert(resp.RequestId, Equals, "863fd73e-be2b-11d3-855b-576787000e19")
}
@@ -327,3 +327,224 @@ var AuthorizeDBSecurityGroupIngressExample = `
</ResponseMetadata>
</AuthorizeDBSecurityGroupIngressResponse>
`
var DescribeDBSubnetGroupsExample = `
<DescribeDBSubnetGroupsResponse xmlns="http://rds.amazonaws.com/doc/2013-09-09/">
<DescribeDBSubnetGroupsResult>
<DBSubnetGroups>
<DBSubnetGroup>
<VpcId>vpc-e7abbdce</VpcId>
<SubnetGroupStatus>Complete</SubnetGroupStatus>
<DBSubnetGroupDescription>DB subnet group 1</DBSubnetGroupDescription>
<DBSubnetGroupName>mydbsubnetgroup1</DBSubnetGroupName>
<Subnets>
<Subnet>
<SubnetStatus>Active</SubnetStatus>
<SubnetIdentifier>subnet-e8b3e5b1</SubnetIdentifier>
<SubnetAvailabilityZone>
<Name>us-west-2a</Name>
<ProvisionedIopsCapable>false</ProvisionedIopsCapable>
</SubnetAvailabilityZone>
</Subnet>
<Subnet>
<SubnetStatus>Active</SubnetStatus>
<SubnetIdentifier>subnet-44b2f22e</SubnetIdentifier>
<SubnetAvailabilityZone>
<Name>us-west-2b</Name>
<ProvisionedIopsCapable>false</ProvisionedIopsCapable>
</SubnetAvailabilityZone>
</Subnet>
</Subnets>
</DBSubnetGroup>
<DBSubnetGroup>
<VpcId>vpc-c1e17bb8</VpcId>
<SubnetGroupStatus>Complete</SubnetGroupStatus>
<DBSubnetGroupDescription>My DB Subnet Group 2</DBSubnetGroupDescription>
<DBSubnetGroupName>sub-grp-2</DBSubnetGroupName>
<Subnets>
<Subnet>
<SubnetStatus>Active</SubnetStatus>
<SubnetIdentifier>subnet-d281ef8a</SubnetIdentifier>
<SubnetAvailabilityZone>
<Name>us-west-2a</Name>
<ProvisionedIopsCapable>false</ProvisionedIopsCapable>
</SubnetAvailabilityZone>
</Subnet>
<Subnet>
<SubnetStatus>Active</SubnetStatus>
<SubnetIdentifier>subnet-b381ef9f</SubnetIdentifier>
<SubnetAvailabilityZone>
<Name>us-west-2c</Name>
<ProvisionedIopsCapable>false</ProvisionedIopsCapable>
</SubnetAvailabilityZone>
</Subnet>
<Subnet>
<SubnetStatus>Active</SubnetStatus>
<SubnetIdentifier>subnet-e1e17ebd</SubnetIdentifier>
<SubnetAvailabilityZone>
<Name>us-west-2b</Name>
<ProvisionedIopsCapable>false</ProvisionedIopsCapable>
</SubnetAvailabilityZone>
</Subnet>
</Subnets>
</DBSubnetGroup>
</DBSubnetGroups>
</DescribeDBSubnetGroupsResult>
<ResponseMetadata>
<RequestId>b783db3b-b98c-11d3-fbc7-5c0aad74da7c</RequestId>
</ResponseMetadata>
</DescribeDBSubnetGroupsResponse>
`
var DeleteDBSubnetGroupExample = `
<DeleteDBSubnetGroupResponse xmlns="http://rds.amazonaws.com/doc/2013-09-09/">
<ResponseMetadata>
<RequestId>6295e5ab-bbf3-11d3-f4c6-37db295f7674</RequestId>
</ResponseMetadata>
</DeleteDBSubnetGroupResponse>
`
var CreateDBSubnetGroupExample = `
<CreateDBSubnetGroupResponse xmlns="http://rds.amazonaws.com/doc/2013-09-09/">
<CreateDBSubnetGroupResult>
<DBSubnetGroup>
<VpcId>vpc-33dc97ea</VpcId>
<SubnetGroupStatus>Complete</SubnetGroupStatus>
<DBSubnetGroupDescription>My new DB Subnet Group</DBSubnetGroupDescription>
<DBSubnetGroupName>myawsuser-dbsubnetgroup</DBSubnetGroupName>
<Subnets>
<Subnet>
<SubnetStatus>Active</SubnetStatus>
<SubnetIdentifier>subnet-e4d398a1</SubnetIdentifier>
<SubnetAvailabilityZone>
<Name>us-east-1b</Name>
<ProvisionedIopsCapable>false</ProvisionedIopsCapable>
</SubnetAvailabilityZone>
</Subnet>
<Subnet>
<SubnetStatus>Active</SubnetStatus>
<SubnetIdentifier>subnet-c2bdb6ba</SubnetIdentifier>
<SubnetAvailabilityZone>
<Name>us-east-1c</Name>
<ProvisionedIopsCapable>false</ProvisionedIopsCapable>
</SubnetAvailabilityZone>
</Subnet>
</Subnets>
</DBSubnetGroup>
</CreateDBSubnetGroupResult>
<ResponseMetadata>
<RequestId>3a401b3f-bb9e-11d3-f4c6-37db295f7674</RequestId>
</ResponseMetadata>
</CreateDBSubnetGroupResponse>
`
var DescribeDBSnapshotsExample = `
<DescribeDBSnapshotsResponse xmlns="http://rds.amazonaws.com/doc/2013-09-09/">
<DescribeDBSnapshotsResult>
<DBSnapshots>
<DBSnapshot>
<Port>3306</Port>
<OptionGroupName>default:mysql-5-6</OptionGroupName>
<Engine>mysql</Engine>
<Status>available</Status>
<SnapshotType>manual</SnapshotType>
<LicenseModel>general-public-license</LicenseModel>
<EngineVersion>5.6.13</EngineVersion>
<DBInstanceIdentifier>my-mysqlexampledb</DBInstanceIdentifier>
<DBSnapshotIdentifier>my-test-restore-snapshot</DBSnapshotIdentifier>
<SnapshotCreateTime>2014-03-28T19:57:16.707Z</SnapshotCreateTime>
<AvailabilityZone>us-west-2b</AvailabilityZone>
<InstanceCreateTime>2014-01-29T22:58:24.231Z</InstanceCreateTime>
<PercentProgress>100</PercentProgress>
<AllocatedStorage>5</AllocatedStorage>
<MasterUsername>awsmyuser</MasterUsername>
</DBSnapshot>
<DBSnapshot>
<Port>3306</Port>
<OptionGroupName>default:mysql-5-6</OptionGroupName>
<Engine>mysql</Engine>
<Status>available</Status>
<SnapshotType>automated</SnapshotType>
<LicenseModel>general-public-license</LicenseModel>
<EngineVersion>5.6.13</EngineVersion>
<DBInstanceIdentifier>my-mysqlexampledb</DBInstanceIdentifier>
<DBSnapshotIdentifier>rds:my-mysqlexampledb-2014-04-19-10-08</DBSnapshotIdentifier>
<SnapshotCreateTime>2014-04-19T10:09:09.790Z</SnapshotCreateTime>
<AvailabilityZone>us-west-2b</AvailabilityZone>
<InstanceCreateTime>2014-01-29T22:58:24.231Z</InstanceCreateTime>
<PercentProgress>100</PercentProgress>
<AllocatedStorage>5</AllocatedStorage>
<MasterUsername>awsmyuser</MasterUsername>
</DBSnapshot>
<DBSnapshot>
<Port>3306</Port>
<OptionGroupName>default:mysql-5-6</OptionGroupName>
<Engine>mysql</Engine>
<Status>available</Status>
<SnapshotType>automated</SnapshotType>
<LicenseModel>general-public-license</LicenseModel>
<EngineVersion>5.6.13</EngineVersion>
<DBInstanceIdentifier>my-mysqlexampledb</DBInstanceIdentifier>
<DBSnapshotIdentifier>rds:my-mysqlexampledb-2014-04-20-10-09</DBSnapshotIdentifier>
<SnapshotCreateTime>2014-04-20T10:09:15.446Z</SnapshotCreateTime>
<AvailabilityZone>us-west-2b</AvailabilityZone>
<InstanceCreateTime>2014-01-29T22:58:24.231Z</InstanceCreateTime>
<PercentProgress>100</PercentProgress>
<AllocatedStorage>5</AllocatedStorage>
<MasterUsername>awsmyuser</MasterUsername>
</DBSnapshot>
</DBSnapshots>
</DescribeDBSnapshotsResult>
<ResponseMetadata>
<RequestId>b7769930-b98c-11d3-f272-7cd6cce12cc5</RequestId>
</ResponseMetadata>
</DescribeDBSnapshotsResponse>
`
var RestoreDBInstanceFromDBSnapshotExample = `
<RestoreDBInstanceFromDBSnapshotResponse xmlns="http://rds.amazonaws.com/doc/2013-09-09/">
<RestoreDBInstanceFromDBSnapshotResult>
<DBInstance>
<BackupRetentionPeriod>2</BackupRetentionPeriod>
<MultiAZ>false</MultiAZ>
<DBInstanceStatus>creating</DBInstanceStatus>
<VpcSecurityGroups/>
<DBInstanceIdentifier>mysqldb-restored</DBInstanceIdentifier>
<PreferredBackupWindow>08:14-08:44</PreferredBackupWindow>
<PreferredMaintenanceWindow>fri:04:50-fri:05:20</PreferredMaintenanceWindow>
<ReadReplicaDBInstanceIdentifiers/>
<Engine>mysql</Engine>
<PendingModifiedValues/>
<LicenseModel>general-public-license</LicenseModel>
<EngineVersion>5.6.13</EngineVersion>
<DBParameterGroups>
<DBParameterGroup>
<ParameterApplyStatus>in-sync</ParameterApplyStatus>
<DBParameterGroupName>default.mysql5.6</DBParameterGroupName>
</DBParameterGroup>
</DBParameterGroups>
<OptionGroupMemberships>
<OptionGroupMembership>
<OptionGroupName>default:mysql-5-6</OptionGroupName>
<Status>pending-apply</Status>
</OptionGroupMembership>
</OptionGroupMemberships>
<PubliclyAccessible>true</PubliclyAccessible>
<DBSecurityGroups>
<DBSecurityGroup>
<Status>active</Status>
<DBSecurityGroupName>default</DBSecurityGroupName>
</DBSecurityGroup>
</DBSecurityGroups>
<DBName>mysqldb</DBName>
<AutoMinorVersionUpgrade>true</AutoMinorVersionUpgrade>
<AllocatedStorage>100</AllocatedStorage>
<MasterUsername>myawsuser</MasterUsername>
<DBInstanceClass>db.m1.medium</DBInstanceClass>
</DBInstance>
</RestoreDBInstanceFromDBSnapshotResult>
<ResponseMetadata>
<RequestId>863fd73e-be2b-11d3-855b-576787000e19</RequestId>
</ResponseMetadata>
</RestoreDBInstanceFromDBSnapshotResponse>
`
@@ -94,3 +94,29 @@ var ListResourceRecordSetsExample = `<?xml version="1.0" encoding="UTF-8"?>
<NextRecordName>testdoc2.example.com</NextRecordName>
<NextRecordType>NS</NextRecordType>
</ListResourceRecordSetsResponse>`
var ListHostedZonesExample = `<?xml version="1.0" encoding="utf-8"?>
<ListHostedZonesResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<HostedZones>
<HostedZone>
<Id>/hostedzone/Z2K123214213123</Id>
<Name>example.com.</Name>
<CallerReference>D2224C5B-684A-DB4A-BB9A-E09E3BAFEA7A</CallerReference>
<Config>
<Comment>Test comment</Comment>
</Config>
<ResourceRecordSetCount>10</ResourceRecordSetCount>
</HostedZone>
<HostedZone>
<Id>/hostedzone/ZLT12321321124</Id>
<Name>sub.example.com.</Name>
<CallerReference>A970F076-FCB1-D959-B395-96474CC84EB8</CallerReference>
<Config>
<Comment>Test comment for subdomain host</Comment>
</Config>
<ResourceRecordSetCount>4</ResourceRecordSetCount>
</HostedZone>
</HostedZones>
<IsTruncated>false</IsTruncated>
<MaxItems>100</MaxItems>
</ListHostedZonesResponse>`
@@ -95,6 +95,16 @@ func (r *Route53) query(method, path string, req, resp interface{}) error {
if err := enc.EncodeElement(req, start); err != nil {
return err
}
// This is really a sadness, but can't think of a better way to
// do this for now in Go's constructs.
replace := "<ResourceRecords><ResourceRecord></ResourceRecord></ResourceRecords>"
if strings.Contains(bodyBuf.String(), replace) {
var newBuf bytes.Buffer
newBuf.WriteString(strings.Replace(bodyBuf.String(), replace, "", -1))
bodyBuf = &newBuf
}
body = bodyBuf
}
@@ -196,6 +206,33 @@ func (r *Route53) GetHostedZone(ID string) (*GetHostedZoneResponse, error) {
return out, err
}
type ListHostedZonesResponse struct {
HostedZones []HostedZone `xml:"HostedZones>HostedZone"`
Marker string `xml:"Marker"`
IsTruncated bool `xml:"IsTruncated"`
NextMarker string `xml:"NextMarker"`
MaxItems int `xml:"MaxItems"`
}
func (r *Route53) ListHostedZones(marker string, maxItems int) (*ListHostedZonesResponse, error) {
values := url.Values{}
if marker != "" {
values.Add("marker", marker)
}
if maxItems != 0 {
values.Add("maxItems", strconv.Itoa(maxItems))
}
out := &ListHostedZonesResponse{}
err := r.query("GET", fmt.Sprintf("/%s/hostedzone/", APIVersion), values, out)
if err != nil {
return nil, err
}
return out, err
}
type GetChangeResponse struct {
ChangeInfo ChangeInfo `xml:"ChangeInfo"`
}
@@ -232,10 +269,29 @@ type ChangeResourceRecordSetsResponse struct {
func (r *Route53) ChangeResourceRecordSets(zone string,
req *ChangeResourceRecordSetsRequest) (*ChangeResourceRecordSetsResponse, error) {
// This is really sad, but we have to format this differently
// for Route53 to make them happy.
reqCopy := *req
for i, change := range reqCopy.Changes {
if len(change.Record.Records) > 1 {
var buf bytes.Buffer
for _, r := range change.Record.Records {
buf.WriteString(fmt.Sprintf(
"<ResourceRecord><Value>%s</Value></ResourceRecord>",
r))
}
change.Record.Records = nil
change.Record.RecordsXML = fmt.Sprintf(
"<ResourceRecords>%s</ResourceRecords>", buf.String())
reqCopy.Changes[i] = change
}
}
zone = CleanZoneID(zone)
out := &ChangeResourceRecordSetsResponse{}
if err := r.query("POST", fmt.Sprintf("/%s/hostedzone/%s/rrset", APIVersion,
zone), req, out); err != nil {
zone), reqCopy, out); err != nil {
return nil, err
}
return out, nil
@@ -261,13 +317,15 @@ type ResourceRecordSet struct {
Name string `xml:"Name"`
Type string `xml:"Type"`
TTL int `xml:"TTL"`
Records []string `xml:"ResourceRecords>ResourceRecord>Value"`
Records []string `xml:"ResourceRecords>ResourceRecord>Value,omitempty"`
SetIdentifier string `xml:"SetIdentifier,omitempty"`
Weight int `xml:"Weight,omitempty"`
HealthCheckId string `xml:"HealthCheckId,omitempty"`
Region string `xml:"Region,omitempty"`
Failover string `xml:"Failover,omitempty"`
AliasTarget *AliasTarget `xml:"AliasTarget,omitempty"`
RecordsXML string `xml:",innerxml"`
}
func (r *Route53) ListResourceRecordSets(zone string, lopts *ListOpts) (*ListResourceRecordSetsResponse, error) {
@@ -157,6 +157,25 @@ func TestListResourceRecordSets(t *testing.T) {
}
}
func TestListHostedZones(t *testing.T) {
testServer := makeTestServer()
client := makeClient(testServer)
testServer.Response(200, nil, ListHostedZonesExample)
resp, err := client.ListHostedZones("", 0)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.HostedZones[0].Name != "example.com." {
t.Fatalf("bad: %v", resp)
}
if resp.HostedZones[1].Name != "sub.example.com." {
t.Fatalf("bad: %v", resp)
}
}
func decode(t *testing.T, r io.Reader, out interface{}) {
var buf1 bytes.Buffer
var buf2 bytes.Buffer
@@ -619,7 +619,7 @@ type request struct {
// amazonShouldEscape returns true if byte should be escaped
func amazonShouldEscape(c byte) bool {
return !((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '~' || c == '.' || c == '/')
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '~' || c == '.' || c == '/' || c == ':')
}
// amazonEscape does uri escaping exactly as Amazon does
@@ -706,22 +706,6 @@ func (s3 *S3) prepare(req *request) error {
req.path = "/" + req.path
}
req.signpath = req.path
// signpath includes subresource, if present: "?acl", "?delete", "?location", "?logging", or "?torrent"
if req.params["acl"] != nil {
req.signpath += "?acl"
}
if req.params["delete"] != nil {
req.signpath += "?delete"
}
if req.params["location"] != nil {
req.signpath += "?location"
}
if req.params["logging"] != nil {
req.signpath += "?logging"
}
if req.params["torrent"] != nil {
req.signpath += "?torrent"
}
if req.bucket != "" {
req.baseurl = s3.Region.S3BucketEndpoint
@@ -427,3 +427,9 @@ func (s *S) TestGetKey(c *C) {
c.Assert(key.Size, Equals, int64(434234))
c.Assert(key.ETag, Equals, GetKeyHeaderDump["ETag"])
}
func (s *S) TestUnescapedColon(c *C) {
b := s.s3.Bucket("bucket")
u := b.URL("foo:bar")
c.Assert(u, Equals, "http://localhost:4444/bucket/foo:bar")
}
@@ -4,10 +4,11 @@ import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"github.com/mitchellh/goamz/aws"
"log"
"sort"
"strings"
"github.com/mitchellh/goamz/aws"
)
var b64 = base64.StdEncoding
@@ -17,6 +18,7 @@ var b64 = base64.StdEncoding
var s3ParamsToSign = map[string]bool{
"acl": true,
"delete": true,
"location": true,
"logging": true,
"notification": true,
@@ -47,6 +49,11 @@ func sign(auth aws.Auth, method, canonicalPath string, params, headers map[strin
headers["x-amz-security-token"] = []string{auth.Token}
}
if auth.SecretKey == "" {
// no auth secret; skip signing, e.g. for public read-only buckets.
return
}
for k, v := range headers {
k = strings.ToLower(k)
switch k {
@@ -111,6 +118,7 @@ func sign(auth aws.Auth, method, canonicalPath string, params, headers map[strin
} else {
headers["Authorization"] = []string{"AWS " + auth.AccessKey + ":" + string(signature)}
}
if debug {
log.Printf("Signature payload: %q", payload)
log.Printf("Signature: %q", signature)
@@ -9,6 +9,7 @@ import (
// S3 ReST authentication docs: http://goo.gl/G1LrK
var testAuth = aws.Auth{"0PN5J17HBGZHT7JJ3X82", "uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o", ""}
var emptyAuth = aws.Auth{"", "", ""}
func (s *S) TestSignExampleObjectGet(c *C) {
method := "GET"
@@ -22,6 +23,17 @@ func (s *S) TestSignExampleObjectGet(c *C) {
c.Assert(headers["Authorization"], DeepEquals, []string{expected})
}
func (s *S) TestSignExampleObjectGetNoAuth(c *C) {
method := "GET"
path := "/johnsmith/photos/puppy.jpg"
headers := map[string][]string{
"Host": {"johnsmith.s3.amazonaws.com"},
"Date": {"Tue, 27 Mar 2007 19:36:42 +0000"},
}
s3.Sign(emptyAuth, method, path, nil, headers)
c.Assert(headers["Authorization"], IsNil)
}
func (s *S) TestSignExampleObjectPut(c *C) {
method := "PUT"
path := "/johnsmith/photos/puppy.jpg"
@@ -54,6 +66,23 @@ func (s *S) TestSignExampleList(c *C) {
c.Assert(headers["Authorization"], DeepEquals, []string{expected})
}
func (s *S) TestSignExampleListNoAuth(c *C) {
method := "GET"
path := "/johnsmith/"
params := map[string][]string{
"prefix": {"photos"},
"max-keys": {"50"},
"marker": {"puppy"},
}
headers := map[string][]string{
"Host": {"johnsmith.s3.amazonaws.com"},
"Date": {"Tue, 27 Mar 2007 19:42:41 +0000"},
"User-Agent": {"Mozilla/5.0"},
}
s3.Sign(emptyAuth, method, path, params, headers)
c.Assert(headers["Authorization"], IsNil)
}
func (s *S) TestSignExampleFetch(c *C) {
method := "GET"
path := "/johnsmith/"
@@ -69,6 +98,20 @@ func (s *S) TestSignExampleFetch(c *C) {
c.Assert(headers["Authorization"], DeepEquals, []string{expected})
}
func (s *S) TestSignExampleFetchNoAuth(c *C) {
method := "GET"
path := "/johnsmith/"
params := map[string][]string{
"acl": {""},
}
headers := map[string][]string{
"Host": {"johnsmith.s3.amazonaws.com"},
"Date": {"Tue, 27 Mar 2007 19:44:46 +0000"},
}
s3.Sign(emptyAuth, method, path, params, headers)
c.Assert(headers["Authorization"], IsNil)
}
func (s *S) TestSignExampleDelete(c *C) {
method := "DELETE"
path := "/johnsmith/photos/puppy.jpg"
@@ -329,6 +329,17 @@ func Lookup(name string) *Flag {
return commandLine.formal[name]
}
// IsSet checks whether flag has been actually set
func (f *FlagSet) IsSet(name string) bool {
_, exists := f.actual[name]
return exists
}
// IsSet checks whether flag has been actually set on the command-lien
func IsSet(name string) bool {
return commandLine.IsSet(name)
}
// Set sets the value of the named flag.
func (f *FlagSet) Set(name, value string) error {
flag, ok := f.formal[name]
@@ -0,0 +1,23 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Andrey Smirnov
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.
@@ -0,0 +1,23 @@
go-ftp-protocol
===============
Plugin for http.Transport with support for ftp:// protocol in Go.
Limitations: only anonymous FTP servers, only file retrieval operations.
Internally connections to FTP servers are cached and re-used when possible.
Example usage:
import "github.com/smira/go-ftp-protocol/protocol"
transport := &http.Transport{}
transport.RegisterProtocol("ftp", &protocol.FTPRoundTripper{})
client := &http.Client{Transport: transport}
resp, err := client.Get("ftp://ftp.ru.debian.org/debian/README")
License: MIT
Base on FTP client library: http://github.com/jlaffaye/ftp/
@@ -0,0 +1,152 @@
// Package protocol implements ftp:// scheme plugin for http.Transport
//
// github.com/jlaffaye/ftp library is used internally as FTP client implementation.
//
// Limitations: only anonymous FTP servers, only file retrieval operations.
//
// Internally connections to FTP servers are cached and re-used when possible.
//
// Example:
//
// transport := &http.Transport{}
// transport.RegisterProtocol("ftp", &FTPRoundTripper{})
// client := &http.Client{Transport: transport}
// resp, err := client.Get("ftp://ftp.ru.debian.org/debian/README")
package protocol
import (
"fmt"
"github.com/jlaffaye/ftp"
"io"
"net/http"
"net/textproto"
"strings"
"sync"
)
// FTPRoundTripper is an implementation of net/http.RoundTripper on top of FTP client
type FTPRoundTripper struct {
lock sync.Mutex
idleConnections map[string][]*ftp.ServerConn
}
// verify interface
var (
_ http.RoundTripper = &FTPRoundTripper{}
)
type readCloserWrapper struct {
body io.ReadCloser
rt *FTPRoundTripper
hostport string
conn *ftp.ServerConn
}
func (w *readCloserWrapper) Read(p []byte) (n int, err error) {
return w.body.Read(p)
}
func (w *readCloserWrapper) Close() error {
err := w.body.Close()
if err == nil {
w.rt.putConnection(w.hostport, w.conn)
}
return err
}
func (rt *FTPRoundTripper) getConnection(hostport string) (conn *ftp.ServerConn, err error) {
rt.lock.Lock()
conns, ok := rt.idleConnections[hostport]
if ok && len(conns) > 0 {
conn = conns[0]
rt.idleConnections[hostport] = conns[1:]
rt.lock.Unlock()
return
}
rt.lock.Unlock()
conn, err = ftp.Connect(hostport)
if err != nil {
return nil, err
}
err = conn.Login("anonymous", "anonymous")
if err != nil {
conn.Quit()
return nil, err
}
return conn, nil
}
func (rt *FTPRoundTripper) putConnection(hostport string, conn *ftp.ServerConn) {
rt.lock.Lock()
defer rt.lock.Unlock()
if rt.idleConnections == nil {
rt.idleConnections = make(map[string][]*ftp.ServerConn)
}
rt.idleConnections[hostport] = append(rt.idleConnections[hostport], conn)
}
// RoundTrip parses incoming GET "HTTP" request and transforms it into
// commands to ftp client
func (rt *FTPRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
if request.URL.Scheme != "ftp" {
return nil, fmt.Errorf("only ftp protocol is supported, got %s", request.Proto)
}
if request.Method != "GET" {
return nil, fmt.Errorf("only GET method is supported, got %s", request.Method)
}
hostport := request.URL.Host
if strings.Index(hostport, ":") == -1 {
hostport = hostport + ":21"
}
connection, err := rt.getConnection(hostport)
if err != nil {
return nil, err
}
var body io.ReadCloser
body, err = connection.Retr(request.URL.Path)
if err != nil {
if te, ok := err.(*textproto.Error); ok {
rt.putConnection(hostport, connection)
if te.Code == ftp.StatusFileUnavailable {
return &http.Response{
Status: "404 Not Found",
StatusCode: 404,
Proto: "FTP/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Request: request,
}, nil
}
}
return nil, err
}
return &http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "FTP/1.0",
ProtoMajor: 1,
ProtoMinor: 0,
Body: &readCloserWrapper{
body: body,
rt: rt,
hostport: hostport,
conn: connection,
},
ContentLength: -1,
Request: request,
}, nil
}
@@ -0,0 +1,94 @@
package protocol
import (
"io/ioutil"
"net/http"
"strings"
"testing"
)
func TestProtocol(t *testing.T) {
transport := &http.Transport{}
transport.RegisterProtocol("ftp", &FTPRoundTripper{})
client := &http.Client{Transport: transport}
resp, err := client.Get("ftp://ftp.ru.debian.org/debian/README")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("resp.StatusCode 200 != %d", resp.StatusCode)
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
err = resp.Body.Close()
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(content), "See http://www.debian.org/ for information about Debian GNU/Linux.") {
t.Fatalf("unexpected content: %s", content)
}
resp, err = client.Get("ftp://ftp.ru.debian.org/debian/missing")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 404 {
t.Fatalf("resp.StatusCode 404 != %d", resp.StatusCode)
}
}
func TestConcurrent(t *testing.T) {
transport := &http.Transport{}
transport.RegisterProtocol("ftp", &FTPRoundTripper{})
client := &http.Client{Transport: transport}
const concurrency = 4
const count = 10
done := make(chan struct{}, concurrency)
for i := 0; i < concurrency; i++ {
go func() {
defer func() { done <- struct{}{} }()
for j := 0; j < 10; j++ {
resp, err := client.Get("ftp://ftp.ru.debian.org/debian/README")
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("resp.StatusCode 200 != %d", resp.StatusCode)
}
content, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
err = resp.Body.Close()
if err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(string(content), "See http://www.debian.org/ for information about Debian GNU/Linux.") {
t.Fatalf("unexpected content: %s", content)
}
}
}()
}
for i := 0; i < concurrency; i++ {
<-done
}
}
@@ -0,0 +1,8 @@
language: go
go:
- 1.2
- 1.3
- tip
script: go test ./...
@@ -1,5 +1,7 @@
This is an implementation of the [LevelDB key/value database](http:code.google.com/p/leveldb) in the [Go programming language](http:golang.org).
[![Build Status](https://travis-ci.org/syndtr/goleveldb.png?branch=master)](https://travis-ci.org/syndtr/goleveldb)
Installation
-----------
@@ -8,7 +10,7 @@ Installation
Requirements
-----------
* Need at least `go1.1` or newer.
* Need at least `go1.2` or newer.
Usage
-----------
@@ -66,6 +68,17 @@ Iterate over subset of database content:
err = iter.Error()
...
Iterate over subset of database content with a particular prefix:
iter := db.NewIterator(util.BytesPrefix([]byte("foo-")), nil)
for iter.Next() {
// Use key/value.
...
}
iter.Release()
err = iter.Error()
...
Batch writes:
batch := new(leveldb.Batch)
@@ -170,7 +170,7 @@ func (p *dbBench) writes(perBatch int) {
b.SetBytes(116)
}
func (p *dbBench) drop() {
func (p *dbBench) gc() {
p.keys, p.values = nil, nil
runtime.GC()
}
@@ -249,6 +249,9 @@ func (p *dbBench) newIter() iterator.Iterator {
}
func (p *dbBench) close() {
if bp, err := p.db.GetProperty("leveldb.blockpool"); err == nil {
p.b.Log("Block pool stats: ", bp)
}
p.db.Close()
p.stor.Close()
os.RemoveAll(benchDB)
@@ -331,7 +334,7 @@ func BenchmarkDBRead(b *testing.B) {
p := openDBBench(b, false)
p.populate(b.N)
p.fill()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -362,7 +365,7 @@ func BenchmarkDBReadUncompressed(b *testing.B) {
p := openDBBench(b, true)
p.populate(b.N)
p.fill()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -379,7 +382,7 @@ func BenchmarkDBReadTable(b *testing.B) {
p.populate(b.N)
p.fill()
p.reopen()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -395,7 +398,7 @@ func BenchmarkDBReadReverse(b *testing.B) {
p := openDBBench(b, false)
p.populate(b.N)
p.fill()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -413,7 +416,7 @@ func BenchmarkDBReadReverseTable(b *testing.B) {
p.populate(b.N)
p.fill()
p.reopen()
p.drop()
p.gc()
iter := p.newIter()
b.ResetTimer()
@@ -11,115 +11,149 @@ import (
"sync/atomic"
)
// SetFunc used by Namespace.Get method to create a cache object. SetFunc
// may return ok false, in that case the cache object will not be created.
type SetFunc func() (ok bool, value interface{}, charge int, fin SetFin)
// SetFunc is the function that will be called by Namespace.Get to create
// a cache object, if charge is less than one than the cache object will
// not be registered to cache tree, if value is nil then the cache object
// will not be created.
type SetFunc func() (charge int, value interface{})
// SetFin will be called when corresponding cache object are released.
type SetFin func()
// DelFin is the function that will be called as the result of a delete operation.
// Exist == true is indication that the object is exist, and pending == true is
// indication of deletion already happen but haven't done yet (wait for all handles
// to be released). And exist == false means the object doesn't exist.
type DelFin func(exist, pending bool)
// DelFin will be called when corresponding cache object are released.
// DelFin will be called after SetFin. The exist is true if the corresponding
// cache object is actually exist in the cache tree.
type DelFin func(exist bool)
// PurgeFin is the function that will be called as the result of a purge operation.
type PurgeFin func(ns, key uint64)
// PurgeFin will be called when corresponding cache object are released.
// PurgeFin will be called after SetFin. If PurgeFin present DelFin will
// not be executed but passed to the PurgeFin, it is up to the caller
// to call it or not.
type PurgeFin func(ns, key uint64, delfin DelFin)
// Cache is a cache tree.
// Cache is a cache tree. A cache instance must be goroutine-safe.
type Cache interface {
// SetCapacity sets cache capacity.
// SetCapacity sets cache tree capacity.
SetCapacity(capacity int)
// GetNamespace gets or creates a cache namespace for the given id.
// Capacity returns cache tree capacity.
Capacity() int
// Used returns used cache tree capacity.
Used() int
// Size returns entire alive cache objects size.
Size() int
// NumObjects returns number of alive objects.
NumObjects() int
// GetNamespace gets cache namespace with the given id.
// GetNamespace is never return nil.
GetNamespace(id uint64) Namespace
// Purge purges all cache namespaces, read Namespace.Purge method documentation.
// PurgeNamespace purges cache namespace with the given id from this cache tree.
// Also read Namespace.Purge.
PurgeNamespace(id uint64, fin PurgeFin)
// ZapNamespace detaches cache namespace with the given id from this cache tree.
// Also read Namespace.Zap.
ZapNamespace(id uint64)
// Purge purges all cache namespace from this cache tree.
// This is behave the same as calling Namespace.Purge method on all cache namespace.
Purge(fin PurgeFin)
// Zap zaps all cache namespaces, read Namespace.Zap method documentation.
Zap(closed bool)
// Zap detaches all cache namespace from this cache tree.
// This is behave the same as calling Namespace.Zap method on all cache namespace.
Zap()
}
// Namespace is a cache namespace.
// Namespace is a cache namespace. A namespace instance must be goroutine-safe.
type Namespace interface {
// Get gets cache object for the given key. The given SetFunc (if not nil) will
// be called if the given key does not exist.
// If the given key does not exist, SetFunc is nil or SetFunc return ok false, Get
// will return ok false.
Get(key uint64, setf SetFunc) (obj Object, ok bool)
// Get deletes cache object for the given key. If exist the cache object will
// be deleted later when all of its handles have been released (i.e. no one use
// it anymore) and the given DelFin (if not nil) will finally be executed. If
// such cache object does not exist the given DelFin will be executed anyway.
// Get gets cache object with the given key.
// If cache object is not found and setf is not nil, Get will atomically creates
// the cache object by calling setf. Otherwise Get will returns nil.
//
// Delete returns true if such cache object exist.
// The returned cache handle should be released after use by calling Release
// method.
Get(key uint64, setf SetFunc) Handle
// Delete removes cache object with the given key from cache tree.
// A deleted cache object will be released as soon as all of its handles have
// been released.
// Delete only happen once, subsequent delete will consider cache object doesn't
// exist, even if the cache object ins't released yet.
//
// If not nil, fin will be called if the cache object doesn't exist or when
// finally be released.
//
// Delete returns true if such cache object exist and never been deleted.
Delete(key uint64, fin DelFin) bool
// Purge deletes all cache objects, read Delete method documentation.
// Purge removes all cache objects within this namespace from cache tree.
// This is the same as doing delete on all cache objects.
//
// If not nil, fin will be called on all cache objects when its finally be
// released.
Purge(fin PurgeFin)
// Zap detaches the namespace from the cache tree and delete all its cache
// objects. The cache objects deletion and finalizers execution are happen
// immediately, even if its existing handles haven't yet been released.
// A zapped namespace can't never be filled again.
// If closed is false then the Get function will always call the given SetFunc
// if it is not nil, but resultant of the SetFunc will not be cached.
Zap(closed bool)
// Zap detaches namespace from cache tree and release all its cache objects.
// A zapped namespace can never be filled again.
// Calling Get on zapped namespace will always return nil.
Zap()
}
// Object is a cache object.
type Object interface {
// Release releases the cache object. Other methods should not be called
// after the cache object has been released.
// Handle is a cache handle.
type Handle interface {
// Release releases this cache handle. This method can be safely called mutiple
// times.
Release()
// Value returns value of the cache object.
// Value returns value of this cache handle.
// Value will returns nil after this cache handle have be released.
Value() interface{}
}
const (
DelNotExist = iota
DelExist
DelPendig
)
// Namespace state.
type nsState int
const (
nsEffective nsState = iota
nsZapped
nsClosed
)
// Node state.
type nodeState int
const (
nodeEffective nodeState = iota
nodeZero nodeState = iota
nodeEffective
nodeEvicted
nodeRemoved
nodeDeleted
)
// Fake object.
type fakeObject struct {
// Fake handle.
type fakeHandle struct {
value interface{}
fin func()
once uint32
}
func (o *fakeObject) Value() interface{} {
if atomic.LoadUint32(&o.once) == 0 {
return o.value
func (h *fakeHandle) Value() interface{} {
if atomic.LoadUint32(&h.once) == 0 {
return h.value
}
return nil
}
func (o *fakeObject) Release() {
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
func (h *fakeHandle) Release() {
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
return
}
if o.fin != nil {
o.fin()
o.fin = nil
if h.fin != nil {
h.fin()
h.fin = nil
}
}
@@ -8,14 +8,32 @@ package cache
import (
"math/rand"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"
)
func set(ns Namespace, key uint64, value interface{}, charge int, fin func()) Object {
obj, _ := ns.Get(key, func() (bool, interface{}, int, SetFin) {
return true, value, charge, fin
type releaserFunc struct {
fn func()
value interface{}
}
func (r releaserFunc) Release() {
if r.fn != nil {
r.fn()
}
}
func set(ns Namespace, key uint64, value interface{}, charge int, relf func()) Handle {
return ns.Get(key, func() (int, interface{}) {
if relf != nil {
return charge, releaserFunc{relf, value}
} else {
return charge, value
}
})
return obj
}
func TestCache_HitMiss(t *testing.T) {
@@ -43,29 +61,31 @@ func TestCache_HitMiss(t *testing.T) {
setfin++
}).Release()
for j, y := range cases {
r, ok := ns.Get(y.key, nil)
h := ns.Get(y.key, nil)
if j <= i {
// should hit
if !ok {
if h == nil {
t.Errorf("case '%d' iteration '%d' is miss", i, j)
} else if r.Value().(string) != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, r.Value().(string), y.value)
} else {
if x := h.Value().(releaserFunc).value.(string); x != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, x, y.value)
}
}
} else {
// should miss
if ok {
t.Errorf("case '%d' iteration '%d' is hit , value '%s'", i, j, r.Value().(string))
if h != nil {
t.Errorf("case '%d' iteration '%d' is hit , value '%s'", i, j, h.Value().(releaserFunc).value.(string))
}
}
if ok {
r.Release()
if h != nil {
h.Release()
}
}
}
for i, x := range cases {
finalizerOk := false
ns.Delete(x.key, func(exist bool) {
ns.Delete(x.key, func(exist, pending bool) {
finalizerOk = true
})
@@ -74,22 +94,24 @@ func TestCache_HitMiss(t *testing.T) {
}
for j, y := range cases {
r, ok := ns.Get(y.key, nil)
h := ns.Get(y.key, nil)
if j > i {
// should hit
if !ok {
if h == nil {
t.Errorf("case '%d' iteration '%d' is miss", i, j)
} else if r.Value().(string) != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, r.Value().(string), y.value)
} else {
if x := h.Value().(releaserFunc).value.(string); x != y.value {
t.Errorf("case '%d' iteration '%d' has invalid value got '%s', want '%s'", i, j, x, y.value)
}
}
} else {
// should miss
if ok {
t.Errorf("case '%d' iteration '%d' is hit, value '%s'", i, j, r.Value().(string))
if h != nil {
t.Errorf("case '%d' iteration '%d' is hit, value '%s'", i, j, h.Value().(releaserFunc).value.(string))
}
}
if ok {
r.Release()
if h != nil {
h.Release()
}
}
}
@@ -107,42 +129,42 @@ func TestLRUCache_Eviction(t *testing.T) {
set(ns, 3, 3, 1, nil).Release()
set(ns, 4, 4, 1, nil).Release()
set(ns, 5, 5, 1, nil).Release()
if r, ok := ns.Get(2, nil); ok { // 1,3,4,5,2
r.Release()
if h := ns.Get(2, nil); h != nil { // 1,3,4,5,2
h.Release()
}
set(ns, 9, 9, 10, nil).Release() // 5,2,9
for _, x := range []uint64{9, 2, 5, 1} {
r, ok := ns.Get(x, nil)
if !ok {
t.Errorf("miss for key '%d'", x)
for _, key := range []uint64{9, 2, 5, 1} {
h := ns.Get(key, nil)
if h == nil {
t.Errorf("miss for key '%d'", key)
} else {
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
o1.Release()
for _, x := range []uint64{1, 2, 5} {
r, ok := ns.Get(x, nil)
if !ok {
t.Errorf("miss for key '%d'", x)
for _, key := range []uint64{1, 2, 5} {
h := ns.Get(key, nil)
if h == nil {
t.Errorf("miss for key '%d'", key)
} else {
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
for _, x := range []uint64{3, 4, 9} {
r, ok := ns.Get(x, nil)
if ok {
t.Errorf("hit for key '%d'", x)
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
for _, key := range []uint64{3, 4, 9} {
h := ns.Get(key, nil)
if h != nil {
t.Errorf("hit for key '%d'", key)
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
}
@@ -153,16 +175,15 @@ func TestLRUCache_SetGet(t *testing.T) {
for i := 0; i < 200; i++ {
n := uint64(rand.Intn(99999) % 20)
set(ns, n, n, 1, nil).Release()
if p, ok := ns.Get(n, nil); ok {
if p.Value() == nil {
if h := ns.Get(n, nil); h != nil {
if h.Value() == nil {
t.Errorf("key '%d' contains nil value", n)
} else {
got := p.Value().(uint64)
if got != n {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", n, n, got)
if x := h.Value().(uint64); x != n {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", n, n, x)
}
}
p.Release()
h.Release()
} else {
t.Errorf("key '%d' doesn't exist", n)
}
@@ -176,31 +197,429 @@ func TestLRUCache_Purge(t *testing.T) {
o2 := set(ns1, 2, 2, 1, nil)
ns1.Purge(nil)
set(ns1, 3, 3, 1, nil).Release()
for _, x := range []uint64{1, 2, 3} {
r, ok := ns1.Get(x, nil)
if !ok {
t.Errorf("miss for key '%d'", x)
for _, key := range []uint64{1, 2, 3} {
h := ns1.Get(key, nil)
if h == nil {
t.Errorf("miss for key '%d'", key)
} else {
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
o1.Release()
o2.Release()
for _, x := range []uint64{1, 2} {
r, ok := ns1.Get(x, nil)
if ok {
t.Errorf("hit for key '%d'", x)
if r.Value().(int) != int(x) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", x, x, r.Value().(int))
for _, key := range []uint64{1, 2} {
h := ns1.Get(key, nil)
if h != nil {
t.Errorf("hit for key '%d'", key)
if x := h.Value().(int); x != int(key) {
t.Errorf("invalid value for key '%d' want '%d', got '%d'", key, key, x)
}
r.Release()
h.Release()
}
}
}
type testingCacheObjectCounter struct {
created uint
released uint
}
func (c *testingCacheObjectCounter) createOne() {
c.created++
}
func (c *testingCacheObjectCounter) releaseOne() {
c.released++
}
type testingCacheObject struct {
t *testing.T
cnt *testingCacheObjectCounter
ns, key uint64
releaseCalled bool
}
func (x *testingCacheObject) Release() {
if !x.releaseCalled {
x.releaseCalled = true
x.cnt.releaseOne()
} else {
x.t.Errorf("duplicate setfin NS#%d KEY#%s", x.ns, x.key)
}
}
func TestLRUCache_ConcurrentSetGet(t *testing.T) {
runtime.GOMAXPROCS(runtime.NumCPU())
seed := time.Now().UnixNano()
t.Logf("seed=%d", seed)
const (
N = 2000000
M = 4000
C = 3
)
var set, get uint32
wg := &sync.WaitGroup{}
c := NewLRUCache(M / 4)
for ni := uint64(0); ni < C; ni++ {
r0 := rand.New(rand.NewSource(seed + int64(ni)))
r1 := rand.New(rand.NewSource(seed + int64(ni) + 1))
ns := c.GetNamespace(ni)
wg.Add(2)
go func(ns Namespace, r *rand.Rand) {
for i := 0; i < N; i++ {
x := uint64(r.Int63n(M))
o := ns.Get(x, func() (int, interface{}) {
atomic.AddUint32(&set, 1)
return 1, x
})
if v := o.Value().(uint64); v != x {
t.Errorf("#%d invalid value, got=%d", x, v)
}
o.Release()
}
wg.Done()
}(ns, r0)
go func(ns Namespace, r *rand.Rand) {
for i := 0; i < N; i++ {
x := uint64(r.Int63n(M))
o := ns.Get(x, nil)
if o != nil {
atomic.AddUint32(&get, 1)
if v := o.Value().(uint64); v != x {
t.Errorf("#%d invalid value, got=%d", x, v)
}
o.Release()
}
}
wg.Done()
}(ns, r1)
}
wg.Wait()
t.Logf("set=%d get=%d", set, get)
}
func TestLRUCache_Finalizer(t *testing.T) {
const (
capacity = 100
goroutines = 100
iterations = 10000
keymax = 8000
)
cnt := &testingCacheObjectCounter{}
c := NewLRUCache(capacity)
type instance struct {
seed int64
rnd *rand.Rand
nsid uint64
ns Namespace
effective int
handles []Handle
handlesMap map[uint64]int
delete bool
purge bool
zap bool
wantDel int
delfinCalled int
delfinCalledAll int
delfinCalledEff int
purgefinCalled int
}
instanceGet := func(p *instance, key uint64) {
h := p.ns.Get(key, func() (charge int, value interface{}) {
to := &testingCacheObject{
t: t, cnt: cnt,
ns: p.nsid,
key: key,
}
p.effective++
cnt.createOne()
return 1, releaserFunc{func() {
to.Release()
p.effective--
}, to}
})
p.handles = append(p.handles, h)
p.handlesMap[key] = p.handlesMap[key] + 1
}
instanceRelease := func(p *instance, i int) {
h := p.handles[i]
key := h.Value().(releaserFunc).value.(*testingCacheObject).key
if n := p.handlesMap[key]; n == 0 {
t.Fatal("key ref == 0")
} else if n > 1 {
p.handlesMap[key] = n - 1
} else {
delete(p.handlesMap, key)
}
h.Release()
p.handles = append(p.handles[:i], p.handles[i+1:]...)
p.handles[len(p.handles) : len(p.handles)+1][0] = nil
}
seed := time.Now().UnixNano()
t.Logf("seed=%d", seed)
instances := make([]*instance, goroutines)
for i := range instances {
p := &instance{}
p.handlesMap = make(map[uint64]int)
p.seed = seed + int64(i)
p.rnd = rand.New(rand.NewSource(p.seed))
p.nsid = uint64(i)
p.ns = c.GetNamespace(p.nsid)
p.delete = i%6 == 0
p.purge = i%8 == 0
p.zap = i%12 == 0 || i%3 == 0
instances[i] = p
}
runr := rand.New(rand.NewSource(seed - 1))
run := func(rnd *rand.Rand, x []*instance, init func(p *instance) bool, fn func(p *instance, i int) bool) {
var (
rx []*instance
rn []int
)
if init == nil {
rx = append([]*instance{}, x...)
rn = make([]int, len(x))
} else {
for _, p := range x {
if init(p) {
rx = append(rx, p)
rn = append(rn, 0)
}
}
}
for len(rx) > 0 {
i := rand.Intn(len(rx))
if fn(rx[i], rn[i]) {
rn[i]++
} else {
rx = append(rx[:i], rx[i+1:]...)
rn = append(rn[:i], rn[i+1:]...)
}
}
}
// Get and release.
run(runr, instances, nil, func(p *instance, i int) bool {
if i < iterations {
if len(p.handles) == 0 || p.rnd.Int()%2 == 0 {
instanceGet(p, uint64(p.rnd.Intn(keymax)))
} else {
instanceRelease(p, p.rnd.Intn(len(p.handles)))
}
return true
} else {
return false
}
})
if used, cap := c.Used(), c.Capacity(); used > cap {
t.Errorf("Used > capacity, used=%d cap=%d", used, cap)
}
// Check effective objects.
for i, p := range instances {
if int(p.effective) < len(p.handlesMap) {
t.Errorf("#%d effective objects < acquired handle, eo=%d ah=%d", i, p.effective, len(p.handlesMap))
}
}
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
// First delete.
run(runr, instances, func(p *instance) bool {
p.wantDel = p.effective
return p.delete
}, func(p *instance, i int) bool {
key := uint64(i)
if key < keymax {
_, wantExist := p.handlesMap[key]
gotExist := p.ns.Delete(key, func(exist, pending bool) {
p.delfinCalledAll++
if exist {
p.delfinCalledEff++
}
})
if !gotExist && wantExist {
t.Errorf("delete on NS#%d KEY#%d not found", p.nsid, key)
}
return true
} else {
return false
}
})
// Second delete.
run(runr, instances, func(p *instance) bool {
p.delfinCalled = 0
return p.delete
}, func(p *instance, i int) bool {
key := uint64(i)
if key < keymax {
gotExist := p.ns.Delete(key, func(exist, pending bool) {
if exist && !pending {
t.Errorf("delete fin on NS#%d KEY#%d exist and not pending for deletion", p.nsid, key)
}
p.delfinCalled++
})
if gotExist {
t.Errorf("delete on NS#%d KEY#%d found", p.nsid, key)
}
return true
} else {
if p.delfinCalled != keymax {
t.Errorf("(2) #%d not all delete fin called, diff=%d", p.ns, keymax-p.delfinCalled)
}
return false
}
})
// Purge.
run(runr, instances, func(p *instance) bool {
return p.purge
}, func(p *instance, i int) bool {
p.ns.Purge(func(ns, key uint64) {
p.purgefinCalled++
})
return false
})
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
// Release.
run(runr, instances, func(p *instance) bool {
return !p.zap
}, func(p *instance, i int) bool {
if len(p.handles) > 0 {
instanceRelease(p, len(p.handles)-1)
return true
} else {
return false
}
})
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
// Zap.
run(runr, instances, func(p *instance) bool {
return p.zap
}, func(p *instance, i int) bool {
p.ns.Zap()
p.handles = nil
p.handlesMap = nil
return false
})
if want := int(cnt.created - cnt.released); c.Size() != want {
t.Errorf("Invalid cache size, want=%d got=%d", want, c.Size())
}
if notrel, used := int(cnt.created-cnt.released), c.Used(); notrel != used {
t.Errorf("Invalid used value, want=%d got=%d", notrel, used)
}
c.Purge(nil)
for _, p := range instances {
if p.delete {
if p.delfinCalledAll != keymax {
t.Errorf("#%d not all delete fin called, purge=%v zap=%v diff=%d", p.nsid, p.purge, p.zap, keymax-p.delfinCalledAll)
}
if p.delfinCalledEff != p.wantDel {
t.Errorf("#%d not all effective delete fin called, diff=%d", p.nsid, p.wantDel-p.delfinCalledEff)
}
if p.purge && p.purgefinCalled > 0 {
t.Errorf("#%d some purge fin called, delete=%v zap=%v n=%d", p.nsid, p.delete, p.zap, p.purgefinCalled)
}
} else {
if p.purge {
if p.purgefinCalled != p.wantDel {
t.Errorf("#%d not all purge fin called, delete=%v zap=%v diff=%d", p.nsid, p.delete, p.zap, p.wantDel-p.purgefinCalled)
}
}
}
}
if cnt.created != cnt.released {
t.Errorf("Some cache object weren't released, created=%d released=%d", cnt.created, cnt.released)
}
}
func BenchmarkLRUCache_Set(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, "", 1, nil)
}
}
func BenchmarkLRUCache_Get(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, "", 1, nil)
}
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
ns.Get(i, nil)
}
}
func BenchmarkLRUCache_Get2(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, "", 1, nil)
}
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
ns.Get(i, func() (charge int, value interface{}) {
return 0, nil
})
}
}
func BenchmarkLRUCache_Release(b *testing.B) {
c := NewLRUCache(0)
ns := c.GetNamespace(0)
handles := make([]Handle, b.N)
for i := uint64(0); i < uint64(b.N); i++ {
handles[i] = set(ns, i, "", 1, nil)
}
b.ResetTimer()
for _, h := range handles {
h.Release()
}
}
func BenchmarkLRUCache_SetRelease(b *testing.B) {
capacity := b.N / 100
if capacity <= 0 {
@@ -210,7 +629,7 @@ func BenchmarkLRUCache_SetRelease(b *testing.B) {
ns := c.GetNamespace(0)
b.ResetTimer()
for i := uint64(0); i < uint64(b.N); i++ {
set(ns, i, nil, 1, nil).Release()
set(ns, i, "", 1, nil).Release()
}
}
@@ -227,10 +646,10 @@ func BenchmarkLRUCache_SetReleaseTwice(b *testing.B) {
nb := b.N - na
for i := uint64(0); i < uint64(na); i++ {
set(ns, i, nil, 1, nil).Release()
set(ns, i, "", 1, nil).Release()
}
for i := uint64(0); i < uint64(nb); i++ {
set(ns, i, nil, 1, nil).Release()
set(ns, i, "", 1, nil).Release()
}
}
@@ -1,246 +0,0 @@
// Copyright (c) 2013, Suryandaru Triandana <syndtr@gmail.com>
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package cache
import (
"sync"
"sync/atomic"
)
type emptyCache struct {
sync.Mutex
table map[uint64]*emptyNS
}
// NewEmptyCache creates a new initialized empty cache.
func NewEmptyCache() Cache {
return &emptyCache{
table: make(map[uint64]*emptyNS),
}
}
func (c *emptyCache) GetNamespace(id uint64) Namespace {
c.Lock()
defer c.Unlock()
if ns, ok := c.table[id]; ok {
return ns
}
ns := &emptyNS{
cache: c,
id: id,
table: make(map[uint64]*emptyNode),
}
c.table[id] = ns
return ns
}
func (c *emptyCache) Purge(fin PurgeFin) {
c.Lock()
for _, ns := range c.table {
ns.purgeNB(fin)
}
c.Unlock()
}
func (c *emptyCache) Zap(closed bool) {
c.Lock()
for _, ns := range c.table {
ns.zapNB(closed)
}
c.table = make(map[uint64]*emptyNS)
c.Unlock()
}
func (*emptyCache) SetCapacity(capacity int) {}
type emptyNS struct {
cache *emptyCache
id uint64
table map[uint64]*emptyNode
state nsState
}
func (ns *emptyNS) Get(key uint64, setf SetFunc) (o Object, ok bool) {
ns.cache.Lock()
switch ns.state {
case nsZapped:
ns.cache.Unlock()
if setf == nil {
return
}
var value interface{}
var fin func()
ok, value, _, fin = setf()
if ok {
o = &fakeObject{
value: value,
fin: fin,
}
}
return
case nsClosed:
ns.cache.Unlock()
return
}
n, ok := ns.table[key]
if ok {
n.ref++
} else {
if setf == nil {
ns.cache.Unlock()
return
}
var value interface{}
var fin func()
ok, value, _, fin = setf()
if !ok {
ns.cache.Unlock()
return
}
n = &emptyNode{
ns: ns,
key: key,
value: value,
setfin: fin,
ref: 1,
}
ns.table[key] = n
}
ns.cache.Unlock()
o = &emptyObject{node: n}
return
}
func (ns *emptyNS) Delete(key uint64, fin DelFin) bool {
ns.cache.Lock()
if ns.state != nsEffective {
ns.cache.Unlock()
if fin != nil {
fin(false)
}
return false
}
n, ok := ns.table[key]
if !ok {
ns.cache.Unlock()
if fin != nil {
fin(false)
}
return false
}
n.delfin = fin
ns.cache.Unlock()
return true
}
func (ns *emptyNS) purgeNB(fin PurgeFin) {
if ns.state != nsEffective {
return
}
for _, n := range ns.table {
n.purgefin = fin
}
}
func (ns *emptyNS) Purge(fin PurgeFin) {
ns.cache.Lock()
ns.purgeNB(fin)
ns.cache.Unlock()
}
func (ns *emptyNS) zapNB(closed bool) {
if ns.state != nsEffective {
return
}
for _, n := range ns.table {
n.execFin()
}
if closed {
ns.state = nsClosed
} else {
ns.state = nsZapped
}
ns.table = nil
}
func (ns *emptyNS) Zap(closed bool) {
ns.cache.Lock()
ns.zapNB(closed)
delete(ns.cache.table, ns.id)
ns.cache.Unlock()
}
type emptyNode struct {
ns *emptyNS
key uint64
value interface{}
ref int
setfin SetFin
delfin DelFin
purgefin PurgeFin
}
func (n *emptyNode) execFin() {
if n.setfin != nil {
n.setfin()
n.setfin = nil
}
if n.purgefin != nil {
n.purgefin(n.ns.id, n.key, n.delfin)
n.delfin = nil
n.purgefin = nil
} else if n.delfin != nil {
n.delfin(true)
n.delfin = nil
}
}
func (n *emptyNode) evict() {
n.ns.cache.Lock()
n.ref--
if n.ref == 0 {
if n.ns.state == nsEffective {
// Remove elem.
delete(n.ns.table, n.key)
// Execute finalizer.
n.execFin()
}
} else if n.ref < 0 {
panic("leveldb/cache: emptyNode: negative node reference")
}
n.ns.cache.Unlock()
}
type emptyObject struct {
node *emptyNode
once uint32
}
func (o *emptyObject) Value() interface{} {
if atomic.LoadUint32(&o.once) == 0 {
return o.node.value
}
return nil
}
func (o *emptyObject) Release() {
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
return
}
o.node.evict()
o.node = nil
}
@@ -9,16 +9,24 @@ package cache
import (
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb/util"
)
// The LLRB implementation were taken from https://github.com/petar/GoLLRB.
// Which contains the following header:
//
// Copyright 2010 Petar Maymounkov. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// lruCache represent a LRU cache state.
type lruCache struct {
sync.Mutex
recent lruNode
table map[uint64]*lruNs
capacity int
size int
mu sync.Mutex
recent lruNode
table map[uint64]*lruNs
capacity int
used, size, alive int
}
// NewLRUCache creates a new initialized LRU cache with the given capacity.
@@ -32,245 +40,415 @@ func NewLRUCache(capacity int) Cache {
return c
}
func (c *lruCache) Capacity() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.capacity
}
func (c *lruCache) Used() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.used
}
func (c *lruCache) Size() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.size
}
func (c *lruCache) NumObjects() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.alive
}
// SetCapacity set cache capacity.
func (c *lruCache) SetCapacity(capacity int) {
c.Lock()
c.mu.Lock()
c.capacity = capacity
c.evict()
c.Unlock()
c.mu.Unlock()
}
// GetNamespace return namespace object for given id.
func (c *lruCache) GetNamespace(id uint64) Namespace {
c.Lock()
defer c.Unlock()
c.mu.Lock()
defer c.mu.Unlock()
if p, ok := c.table[id]; ok {
return p
if ns, ok := c.table[id]; ok {
return ns
}
p := &lruNs{
lru: c,
id: id,
table: make(map[uint64]*lruNode),
ns := &lruNs{lru: c, id: id}
c.table[id] = ns
return ns
}
func (c *lruCache) ZapNamespace(id uint64) {
c.mu.Lock()
if ns, exist := c.table[id]; exist {
ns.zapNB()
delete(c.table, id)
}
c.table[id] = p
return p
c.mu.Unlock()
}
func (c *lruCache) PurgeNamespace(id uint64, fin PurgeFin) {
c.mu.Lock()
if ns, exist := c.table[id]; exist {
ns.purgeNB(fin)
}
c.mu.Unlock()
}
// Purge purge entire cache.
func (c *lruCache) Purge(fin PurgeFin) {
c.Lock()
c.mu.Lock()
for _, ns := range c.table {
ns.purgeNB(fin)
}
c.Unlock()
c.mu.Unlock()
}
func (c *lruCache) Zap(closed bool) {
c.Lock()
func (c *lruCache) Zap() {
c.mu.Lock()
for _, ns := range c.table {
ns.zapNB(closed)
ns.zapNB()
}
c.table = make(map[uint64]*lruNs)
c.Unlock()
c.mu.Unlock()
}
func (c *lruCache) evict() {
top := &c.recent
for n := c.recent.rPrev; c.size > c.capacity && n != top; {
for n := c.recent.rPrev; c.used > c.capacity && n != top; {
if n.state != nodeEffective {
panic("evicting non effective node")
}
n.state = nodeEvicted
n.rRemove()
n.evictNB()
c.size -= n.charge
n.derefNB()
c.used -= n.charge
n = c.recent.rPrev
}
}
type lruNs struct {
lru *lruCache
id uint64
table map[uint64]*lruNode
state nsState
lru *lruCache
id uint64
rbRoot *lruNode
state nsState
}
func (ns *lruNs) Get(key uint64, setf SetFunc) (o Object, ok bool) {
lru := ns.lru
lru.Lock()
switch ns.state {
case nsZapped:
lru.Unlock()
if setf == nil {
return
}
var value interface{}
var fin func()
ok, value, _, fin = setf()
if ok {
o = &fakeObject{
value: value,
fin: fin,
}
}
return
case nsClosed:
lru.Unlock()
return
func (ns *lruNs) rbGetOrCreateNode(h *lruNode, key uint64) (hn, n *lruNode) {
if h == nil {
n = &lruNode{ns: ns, key: key}
return n, n
}
n, ok := ns.table[key]
if ok {
switch n.state {
case nodeEvicted:
// Insert to recent list.
n.state = nodeEffective
n.ref++
lru.size += n.charge
lru.evict()
fallthrough
case nodeEffective:
// Bump to front
n.rRemove()
n.rInsert(&lru.recent)
if key < h.key {
hn, n = ns.rbGetOrCreateNode(h.rbLeft, key)
if hn != nil {
h.rbLeft = hn
} else {
return nil, n
}
} else if key > h.key {
hn, n = ns.rbGetOrCreateNode(h.rbRight, key)
if hn != nil {
h.rbRight = hn
} else {
return nil, n
}
n.ref++
} else {
if setf == nil {
lru.Unlock()
return
}
var value interface{}
var charge int
var fin func()
ok, value, charge, fin = setf()
if !ok {
lru.Unlock()
return
}
n = &lruNode{
ns: ns,
key: key,
value: value,
charge: charge,
setfin: fin,
ref: 2,
}
ns.table[key] = n
n.rInsert(&lru.recent)
lru.size += charge
lru.evict()
return nil, h
}
lru.Unlock()
o = &lruObject{node: n}
return
if rbIsRed(h.rbRight) && !rbIsRed(h.rbLeft) {
h = rbRotLeft(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
h = rbRotRight(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
rbFlip(h)
}
return h, n
}
func (ns *lruNs) getOrCreateNode(key uint64) *lruNode {
hn, n := ns.rbGetOrCreateNode(ns.rbRoot, key)
if hn != nil {
ns.rbRoot = hn
ns.rbRoot.rbBlack = true
}
return n
}
func (ns *lruNs) rbGetNode(key uint64) *lruNode {
h := ns.rbRoot
for h != nil {
switch {
case key < h.key:
h = h.rbLeft
case key > h.key:
h = h.rbRight
default:
return h
}
}
return nil
}
func (ns *lruNs) getNode(key uint64) *lruNode {
return ns.rbGetNode(key)
}
func (ns *lruNs) rbDeleteNode(h *lruNode, key uint64) *lruNode {
if h == nil {
return nil
}
if key < h.key {
if h.rbLeft == nil { // key not present. Nothing to delete
return h
}
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
h = rbMoveLeft(h)
}
h.rbLeft = ns.rbDeleteNode(h.rbLeft, key)
} else {
if rbIsRed(h.rbLeft) {
h = rbRotRight(h)
}
// If @key equals @h.key and no right children at @h
if h.key == key && h.rbRight == nil {
return nil
}
if h.rbRight != nil && !rbIsRed(h.rbRight) && !rbIsRed(h.rbRight.rbLeft) {
h = rbMoveRight(h)
}
// If @key equals @h.key, and (from above) 'h.Right != nil'
if h.key == key {
var x *lruNode
h.rbRight, x = rbDeleteMin(h.rbRight)
if x == nil {
panic("logic")
}
x.rbLeft, h.rbLeft = h.rbLeft, nil
x.rbRight, h.rbRight = h.rbRight, nil
x.rbBlack = h.rbBlack
h = x
} else { // Else, @key is bigger than @h.key
h.rbRight = ns.rbDeleteNode(h.rbRight, key)
}
}
return rbFixup(h)
}
func (ns *lruNs) deleteNode(key uint64) {
ns.rbRoot = ns.rbDeleteNode(ns.rbRoot, key)
if ns.rbRoot != nil {
ns.rbRoot.rbBlack = true
}
}
func (ns *lruNs) rbIterateNodes(h *lruNode, pivot uint64, iter func(n *lruNode) bool) bool {
if h == nil {
return true
}
if h.key >= pivot {
if !ns.rbIterateNodes(h.rbLeft, pivot, iter) {
return false
}
if !iter(h) {
return false
}
}
return ns.rbIterateNodes(h.rbRight, pivot, iter)
}
func (ns *lruNs) iterateNodes(iter func(n *lruNode) bool) {
ns.rbIterateNodes(ns.rbRoot, 0, iter)
}
func (ns *lruNs) Get(key uint64, setf SetFunc) Handle {
ns.lru.mu.Lock()
defer ns.lru.mu.Unlock()
if ns.state != nsEffective {
return nil
}
var n *lruNode
if setf == nil {
n = ns.getNode(key)
if n == nil {
return nil
}
} else {
n = ns.getOrCreateNode(key)
}
switch n.state {
case nodeZero:
charge, value := setf()
if value == nil {
ns.deleteNode(key)
return nil
}
if charge < 0 {
charge = 0
}
n.value = value
n.charge = charge
n.state = nodeEvicted
ns.lru.size += charge
ns.lru.alive++
fallthrough
case nodeEvicted:
if n.charge == 0 {
break
}
// Insert to recent list.
n.state = nodeEffective
n.ref++
ns.lru.used += n.charge
ns.lru.evict()
fallthrough
case nodeEffective:
// Bump to front.
n.rRemove()
n.rInsert(&ns.lru.recent)
case nodeDeleted:
// Do nothing.
default:
panic("invalid state")
}
n.ref++
return &lruHandle{node: n}
}
func (ns *lruNs) Delete(key uint64, fin DelFin) bool {
lru := ns.lru
lru.Lock()
ns.lru.mu.Lock()
defer ns.lru.mu.Unlock()
if ns.state != nsEffective {
lru.Unlock()
if fin != nil {
fin(false)
fin(false, false)
}
return false
}
n, ok := ns.table[key]
if !ok {
lru.Unlock()
n := ns.getNode(key)
if n == nil {
if fin != nil {
fin(false)
fin(false, false)
}
return false
}
n.delfin = fin
switch n.state {
case nodeRemoved:
lru.Unlock()
return false
case nodeEffective:
lru.size -= n.charge
ns.lru.used -= n.charge
n.state = nodeDeleted
n.delfin = fin
n.rRemove()
n.evictNB()
n.derefNB()
case nodeEvicted:
n.state = nodeDeleted
n.delfin = fin
case nodeDeleted:
if fin != nil {
fin(true, true)
}
return false
default:
panic("invalid state")
}
n.state = nodeRemoved
lru.Unlock()
return true
}
func (ns *lruNs) purgeNB(fin PurgeFin) {
lru := ns.lru
if ns.state != nsEffective {
return
}
for _, n := range ns.table {
n.purgefin = fin
if n.state == nodeEffective {
lru.size -= n.charge
n.rRemove()
n.evictNB()
if ns.state == nsEffective {
var nodes []*lruNode
ns.iterateNodes(func(n *lruNode) bool {
nodes = append(nodes, n)
return true
})
for _, n := range nodes {
switch n.state {
case nodeEffective:
ns.lru.used -= n.charge
n.state = nodeDeleted
n.purgefin = fin
n.rRemove()
n.derefNB()
case nodeEvicted:
n.state = nodeDeleted
n.purgefin = fin
case nodeDeleted:
default:
panic("invalid state")
}
}
n.state = nodeRemoved
}
}
func (ns *lruNs) Purge(fin PurgeFin) {
ns.lru.Lock()
ns.lru.mu.Lock()
ns.purgeNB(fin)
ns.lru.Unlock()
ns.lru.mu.Unlock()
}
func (ns *lruNs) zapNB(closed bool) {
lru := ns.lru
if ns.state != nsEffective {
return
}
if closed {
ns.state = nsClosed
} else {
func (ns *lruNs) zapNB() {
if ns.state == nsEffective {
ns.state = nsZapped
ns.iterateNodes(func(n *lruNode) bool {
if n.state == nodeEffective {
ns.lru.used -= n.charge
n.rRemove()
}
ns.lru.size -= n.charge
n.state = nodeDeleted
n.fin()
return true
})
ns.rbRoot = nil
}
for _, n := range ns.table {
if n.state == nodeEffective {
lru.size -= n.charge
n.rRemove()
}
n.state = nodeRemoved
n.execFin()
}
ns.table = nil
}
func (ns *lruNs) Zap(closed bool) {
ns.lru.Lock()
ns.zapNB(closed)
func (ns *lruNs) Zap() {
ns.lru.mu.Lock()
ns.zapNB()
delete(ns.lru.table, ns.id)
ns.lru.Unlock()
ns.lru.mu.Unlock()
}
type lruNode struct {
ns *lruNs
rNext, rPrev *lruNode
rNext, rPrev *lruNode
rbLeft, rbRight *lruNode
rbBlack bool
key uint64
value interface{}
charge int
ref int
state nodeState
setfin SetFin
delfin DelFin
purgefin PurgeFin
}
@@ -284,7 +462,6 @@ func (n *lruNode) rInsert(at *lruNode) {
}
func (n *lruNode) rRemove() bool {
// only remove if not already removed
if n.rPrev == nil {
return false
}
@@ -297,58 +474,149 @@ func (n *lruNode) rRemove() bool {
return true
}
func (n *lruNode) execFin() {
if n.setfin != nil {
n.setfin()
n.setfin = nil
func (n *lruNode) fin() {
if r, ok := n.value.(util.Releaser); ok {
r.Release()
}
if n.purgefin != nil {
n.purgefin(n.ns.id, n.key, n.delfin)
n.delfin = nil
if n.delfin != nil {
panic("conflicting delete and purge fin")
}
n.purgefin(n.ns.id, n.key)
n.purgefin = nil
} else if n.delfin != nil {
n.delfin(true)
n.delfin(true, false)
n.delfin = nil
}
}
func (n *lruNode) evictNB() {
func (n *lruNode) derefNB() {
n.ref--
if n.ref == 0 {
if n.ns.state == nsEffective {
// remove elem
delete(n.ns.table, n.key)
// execute finalizer
n.execFin()
// Remove elemement.
n.ns.deleteNode(n.key)
n.ns.lru.size -= n.charge
n.ns.lru.alive--
n.fin()
}
n.value = nil
} else if n.ref < 0 {
panic("leveldb/cache: lruCache: negative node reference")
}
}
func (n *lruNode) evict() {
n.ns.lru.Lock()
n.evictNB()
n.ns.lru.Unlock()
func (n *lruNode) deref() {
n.ns.lru.mu.Lock()
n.derefNB()
n.ns.lru.mu.Unlock()
}
type lruObject struct {
type lruHandle struct {
node *lruNode
once uint32
}
func (o *lruObject) Value() interface{} {
if atomic.LoadUint32(&o.once) == 0 {
return o.node.value
func (h *lruHandle) Value() interface{} {
if atomic.LoadUint32(&h.once) == 0 {
return h.node.value
}
return nil
}
func (o *lruObject) Release() {
if !atomic.CompareAndSwapUint32(&o.once, 0, 1) {
func (h *lruHandle) Release() {
if !atomic.CompareAndSwapUint32(&h.once, 0, 1) {
return
}
o.node.evict()
o.node = nil
h.node.deref()
h.node = nil
}
func rbIsRed(h *lruNode) bool {
if h == nil {
return false
}
return !h.rbBlack
}
func rbRotLeft(h *lruNode) *lruNode {
x := h.rbRight
if x.rbBlack {
panic("rotating a black link")
}
h.rbRight = x.rbLeft
x.rbLeft = h
x.rbBlack = h.rbBlack
h.rbBlack = false
return x
}
func rbRotRight(h *lruNode) *lruNode {
x := h.rbLeft
if x.rbBlack {
panic("rotating a black link")
}
h.rbLeft = x.rbRight
x.rbRight = h
x.rbBlack = h.rbBlack
h.rbBlack = false
return x
}
func rbFlip(h *lruNode) {
h.rbBlack = !h.rbBlack
h.rbLeft.rbBlack = !h.rbLeft.rbBlack
h.rbRight.rbBlack = !h.rbRight.rbBlack
}
func rbMoveLeft(h *lruNode) *lruNode {
rbFlip(h)
if rbIsRed(h.rbRight.rbLeft) {
h.rbRight = rbRotRight(h.rbRight)
h = rbRotLeft(h)
rbFlip(h)
}
return h
}
func rbMoveRight(h *lruNode) *lruNode {
rbFlip(h)
if rbIsRed(h.rbLeft.rbLeft) {
h = rbRotRight(h)
rbFlip(h)
}
return h
}
func rbFixup(h *lruNode) *lruNode {
if rbIsRed(h.rbRight) {
h = rbRotLeft(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbLeft.rbLeft) {
h = rbRotRight(h)
}
if rbIsRed(h.rbLeft) && rbIsRed(h.rbRight) {
rbFlip(h)
}
return h
}
func rbDeleteMin(h *lruNode) (hn, n *lruNode) {
if h == nil {
return nil, nil
}
if h.rbLeft == nil {
return nil, h
}
if !rbIsRed(h.rbLeft) && !rbIsRed(h.rbLeft.rbLeft) {
h = rbMoveLeft(h)
}
h.rbLeft, n = rbDeleteMin(h.rbLeft)
return rbFixup(h), n
}
@@ -9,34 +9,48 @@ package leveldb
import "github.com/syndtr/goleveldb/leveldb/comparer"
type iComparer struct {
cmp comparer.Comparer
ucmp comparer.Comparer
}
func (p *iComparer) Name() string {
return p.cmp.Name()
func (icmp *iComparer) uName() string {
return icmp.ucmp.Name()
}
func (p *iComparer) Compare(a, b []byte) int {
ia, ib := iKey(a), iKey(b)
r := p.cmp.Compare(ia.ukey(), ib.ukey())
if r == 0 {
an, bn := ia.num(), ib.num()
if an > bn {
r = -1
} else if an < bn {
r = 1
func (icmp *iComparer) uCompare(a, b []byte) int {
return icmp.ucmp.Compare(a, b)
}
func (icmp *iComparer) uSeparator(dst, a, b []byte) []byte {
return icmp.ucmp.Separator(dst, a, b)
}
func (icmp *iComparer) uSuccessor(dst, b []byte) []byte {
return icmp.ucmp.Successor(dst, b)
}
func (icmp *iComparer) Name() string {
return icmp.uName()
}
func (icmp *iComparer) Compare(a, b []byte) int {
x := icmp.ucmp.Compare(iKey(a).ukey(), iKey(b).ukey())
if x == 0 {
if m, n := iKey(a).num(), iKey(b).num(); m > n {
x = -1
} else if m < n {
x = 1
}
}
return r
return x
}
func (p *iComparer) Separator(dst, a, b []byte) []byte {
func (icmp *iComparer) Separator(dst, a, b []byte) []byte {
ua, ub := iKey(a).ukey(), iKey(b).ukey()
dst = p.cmp.Separator(dst, ua, ub)
dst = icmp.ucmp.Separator(dst, ua, ub)
if dst == nil {
return nil
}
if len(dst) < len(ua) && p.cmp.Compare(ua, dst) < 0 {
if len(dst) < len(ua) && icmp.uCompare(ua, dst) < 0 {
dst = append(dst, kMaxNumBytes...)
} else {
// Did not close possibilities that n maybe longer than len(ub).
@@ -45,13 +59,13 @@ func (p *iComparer) Separator(dst, a, b []byte) []byte {
return dst
}
func (p *iComparer) Successor(dst, b []byte) []byte {
func (icmp *iComparer) Successor(dst, b []byte) []byte {
ub := iKey(b).ukey()
dst = p.cmp.Successor(dst, ub)
dst = icmp.ucmp.Successor(dst, ub)
if dst == nil {
return nil
}
if len(dst) < len(ub) && p.cmp.Compare(ub, dst) < 0 {
if len(dst) < len(ub) && icmp.uCompare(ub, dst) < 0 {
dst = append(dst, kMaxNumBytes...)
} else {
// Did not close possibilities that n maybe longer than len(ub).
@@ -176,6 +176,18 @@ func (h *dbCorruptHarness) removeAll(ft storage.FileType) {
}
}
func (h *dbCorruptHarness) removeOne(ft storage.FileType) {
ff, err := h.stor.GetFiles(ft)
if err != nil {
h.t.Fatal("get files: ", err)
}
f := ff[rand.Intn(len(ff))]
h.t.Logf("removing file @%d", f.Num())
if err := f.Remove(); err != nil {
h.t.Error("remove file: ", err)
}
}
func (h *dbCorruptHarness) check(min, max int) {
p := &h.dbHarness
t := p.t
@@ -386,6 +398,33 @@ func TestCorruptDB_UnrelatedKeys(t *testing.T) {
h.close()
}
func TestCorruptDB_Level0NewerFileHasOlderSeqnum(t *testing.T) {
h := newDbCorruptHarness(t)
h.put("a", "v1")
h.put("b", "v1")
h.compactMem()
h.put("a", "v2")
h.put("b", "v2")
h.compactMem()
h.put("a", "v3")
h.put("b", "v3")
h.compactMem()
h.put("c", "v0")
h.put("d", "v0")
h.compactMem()
h.compactRangeAt(1, "", "")
h.closeDB()
h.recover()
h.getVal("a", "v3")
h.getVal("b", "v3")
h.getVal("c", "v0")
h.getVal("d", "v0")
h.close()
}
func TestCorruptDB_RecoverInvalidSeq_Issue53(t *testing.T) {
h := newDbCorruptHarness(t)
@@ -412,3 +451,22 @@ func TestCorruptDB_RecoverInvalidSeq_Issue53(t *testing.T) {
h.close()
}
func TestCorruptDB_MissingTableFiles(t *testing.T) {
h := newDbCorruptHarness(t)
h.put("a", "v1")
h.put("b", "v1")
h.compactMem()
h.put("c", "v2")
h.put("d", "v2")
h.compactMem()
h.put("e", "v3")
h.put("f", "v3")
h.closeDB()
h.removeOne(storage.TypeTable)
h.openAssert(false)
h.close()
}
@@ -14,6 +14,7 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/syndtr/goleveldb/leveldb/iterator"
@@ -25,56 +26,54 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
var (
ErrNotFound = util.ErrNotFound
ErrSnapshotReleased = errors.New("leveldb: snapshot released")
ErrIterReleased = errors.New("leveldb: iterator released")
ErrClosed = errors.New("leveldb: closed")
)
// DB is a LevelDB database.
type DB struct {
// Need 64-bit alignment.
seq uint64
// Session.
s *session
// MemDB
// MemDB.
memMu sync.RWMutex
mem *memdb.DB
frozenMem *memdb.DB
memPool chan *memdb.DB
mem, frozenMem *memDB
journal *journal.Writer
journalWriter storage.Writer
journalFile storage.File
frozenJournalFile storage.File
frozenSeq uint64
// Snapshot
// Snapshot.
snapsMu sync.Mutex
snapsRoot snapshotElement
// Write
writeCh chan *Batch
writeMergedCh chan bool
writeLockCh chan struct{}
writeAckCh chan error
journalCh chan *Batch
journalAckCh chan error
// Stats.
aliveSnaps, aliveIters int32
// Compaction
compCh chan chan<- struct{}
compMemCh chan chan<- struct{}
compMemAckCh chan struct{}
compReqCh chan *cReq
compErrCh chan error
compErrSetCh chan error
compStats [kNumLevels]cStats
// Write.
writeC chan *Batch
writeMergedC chan bool
writeLockC chan struct{}
writeAckC chan error
journalC chan *Batch
journalAckC chan error
// Close
closeWg sync.WaitGroup
closeCh chan struct{}
closed uint32
closer io.Closer
// Compaction.
tcompCmdC chan cCmd
tcompPauseC chan chan<- struct{}
tcompTriggerC chan struct{}
mcompCmdC chan cCmd
mcompTriggerC chan struct{}
compErrC chan error
compErrSetC chan error
compStats [kNumLevels]cStats
// Close.
closeW sync.WaitGroup
closeC chan struct{}
closed uint32
closer io.Closer
}
func openDB(s *session) (*DB, error) {
@@ -84,42 +83,50 @@ func openDB(s *session) (*DB, error) {
s: s,
// Initial sequence
seq: s.stSeq,
// MemDB
memPool: make(chan *memdb.DB, 1),
// Write
writeCh: make(chan *Batch),
writeMergedCh: make(chan bool),
writeLockCh: make(chan struct{}, 1),
writeAckCh: make(chan error),
journalCh: make(chan *Batch),
journalAckCh: make(chan error),
writeC: make(chan *Batch),
writeMergedC: make(chan bool),
writeLockC: make(chan struct{}, 1),
writeAckC: make(chan error),
journalC: make(chan *Batch),
journalAckC: make(chan error),
// Compaction
compCh: make(chan chan<- struct{}, 1),
compMemCh: make(chan chan<- struct{}, 1),
compMemAckCh: make(chan struct{}, 1),
compReqCh: make(chan *cReq),
compErrCh: make(chan error),
compErrSetCh: make(chan error),
tcompCmdC: make(chan cCmd),
tcompPauseC: make(chan chan<- struct{}),
tcompTriggerC: make(chan struct{}, 1),
mcompCmdC: make(chan cCmd),
mcompTriggerC: make(chan struct{}, 1),
compErrC: make(chan error),
compErrSetC: make(chan error),
// Close
closeCh: make(chan struct{}),
closeC: make(chan struct{}),
}
db.initSnapshot()
db.compMemAckCh <- struct{}{}
if err := db.recoverJournal(); err != nil {
return nil, err
}
// Remove any obsolete files.
if err := db.cleanFiles(); err != nil {
if err := db.checkAndCleanFiles(); err != nil {
// Close journal.
if db.journal != nil {
db.journal.Close()
db.journalWriter.Close()
}
return nil, err
}
// Don't include compaction error goroutine into wait group.
go db.compactionError()
db.closeWg.Add(2)
go db.compaction()
go db.writeJournal()
db.wakeCompaction(0)
db.closeW.Add(3)
go db.tCompaction()
go db.mCompaction()
go db.jWriter()
go db.mpoolDrain()
s.logf("db@open done T·%v", time.Since(start))
@@ -132,13 +139,14 @@ func openDB(s *session) (*DB, error) {
// Also, if ErrorIfExist is true and the DB exist Open will returns
// os.ErrExist error.
//
// Open will return an error with type of ErrManifest if manifest file
// is missing or corrupted. Missing or corrupted manifest file can be
// recovered with Recover function.
// Open will return an error with type of ErrCorrupted if corruption
// detected in the DB. Corrupted DB can be recovered with Recover
// function.
//
// The returned DB instance is goroutine-safe.
// The DB must be closed after use, by calling Close method.
func Open(p storage.Storage, o *opt.Options) (db *DB, err error) {
s, err := newSession(p, o)
func Open(stor storage.Storage, o *opt.Options) (db *DB, err error) {
s, err := newSession(stor, o)
if err != nil {
return
}
@@ -174,10 +182,11 @@ func Open(p storage.Storage, o *opt.Options) (db *DB, err error) {
// OpenFile uses standard file-system backed storage implementation as
// desribed in the leveldb/storage package.
//
// OpenFile will return an error with type of ErrManifest if manifest file
// is missing or corrupted. Missing or corrupted manifest file can be
// recovered with Recover function.
// OpenFile will return an error with type of ErrCorrupted if corruption
// detected in the DB. Corrupted DB can be recovered with Recover
// function.
//
// The returned DB instance is goroutine-safe.
// The DB must be closed after use, by calling Close method.
func OpenFile(path string, o *opt.Options) (db *DB, err error) {
stor, err := storage.OpenFile(path)
@@ -198,9 +207,10 @@ func OpenFile(path string, o *opt.Options) (db *DB, err error) {
// The DB must already exist or it will returns an error.
// Also, Recover will ignore ErrorIfMissing and ErrorIfExist options.
//
// The returned DB instance is goroutine-safe.
// The DB must be closed after use, by calling Close method.
func Recover(p storage.Storage, o *opt.Options) (db *DB, err error) {
s, err := newSession(p, o)
func Recover(stor storage.Storage, o *opt.Options) (db *DB, err error) {
s, err := newSession(stor, o)
if err != nil {
return
}
@@ -226,6 +236,7 @@ func Recover(p storage.Storage, o *opt.Options) (db *DB, err error) {
// RecoverFile uses standard file-system backed storage implementation as desribed
// in the leveldb/storage package.
//
// The returned DB instance is goroutine-safe.
// The DB must be closed after use, by calling Close method.
func RecoverFile(path string, o *opt.Options) (db *DB, err error) {
stor, err := storage.OpenFile(path)
@@ -242,16 +253,18 @@ func RecoverFile(path string, o *opt.Options) (db *DB, err error) {
}
func recoverTable(s *session, o *opt.Options) error {
ff0, err := s.getFiles(storage.TypeTable)
// Get all tables and sort it by file number.
tableFiles_, err := s.getFiles(storage.TypeTable)
if err != nil {
return err
}
ff1 := files(ff0)
ff1.sort()
tableFiles := files(tableFiles_)
tableFiles.sort()
var mSeq uint64
var good, corrupted int
rec := new(sessionRecord)
bpool := util.NewBufferPool(o.GetBlockSize() + 5)
buildTable := func(iter iterator.Iterator) (tmp storage.File, size int64, err error) {
tmp = s.newTemp()
writer, err := tmp.Create()
@@ -265,8 +278,9 @@ func recoverTable(s *session, o *opt.Options) error {
tmp = nil
}
}()
// Copy entries.
tw := table.NewWriter(writer, o)
// Copy records.
for iter.Next() {
key := iter.Key()
if validIkey(key) {
@@ -298,20 +312,23 @@ func recoverTable(s *session, o *opt.Options) error {
return err
}
defer reader.Close()
// Get file size.
size, err := reader.Seek(0, 2)
if err != nil {
return err
}
var tSeq uint64
var tgood, tcorrupted, blockerr int
var min, max []byte
tr := table.NewReader(reader, size, nil, o)
var imin, imax []byte
tr := table.NewReader(reader, size, nil, bpool, o)
iter := tr.NewIterator(nil, nil)
iter.(iterator.ErrorCallbackSetter).SetErrorCallback(func(err error) {
s.logf("table@recovery found error @%d %q", file.Num(), err)
blockerr++
})
// Scan the table.
for iter.Next() {
key := iter.Key()
@@ -324,16 +341,17 @@ func recoverTable(s *session, o *opt.Options) error {
if seq > tSeq {
tSeq = seq
}
if min == nil {
min = append([]byte{}, key...)
if imin == nil {
imin = append([]byte{}, key...)
}
max = append(max[:0], key...)
imax = append(imax[:0], key...)
}
if err := iter.Error(); err != nil {
iter.Release()
return err
}
iter.Release()
if tgood > 0 {
if tcorrupted > 0 || blockerr > 0 {
// Rebuild the table.
@@ -354,7 +372,7 @@ func recoverTable(s *session, o *opt.Options) error {
mSeq = tSeq
}
// Add table to level 0.
rec.addTable(0, file.Num(), uint64(size), min, max)
rec.addTable(0, file.Num(), uint64(size), imin, imax)
s.logf("table@recovery recovered @%d N·%d C·%d B·%d S·%d Q·%d", file.Num(), tgood, tcorrupted, blockerr, size, tSeq)
} else {
s.logf("table@recovery unrecoverable @%d C·%d B·%d S·%d", file.Num(), tcorrupted, blockerr, size)
@@ -365,42 +383,56 @@ func recoverTable(s *session, o *opt.Options) error {
return nil
}
// Recover all tables.
if len(ff1) > 0 {
s.logf("table@recovery F·%d", len(ff1))
s.markFileNum(ff1[len(ff1)-1].Num())
for _, file := range ff1 {
if len(tableFiles) > 0 {
s.logf("table@recovery F·%d", len(tableFiles))
// Mark file number as used.
s.markFileNum(tableFiles[len(tableFiles)-1].Num())
for _, file := range tableFiles {
if err := recoverTable(file); err != nil {
return err
}
}
s.logf("table@recovery recovered F·%d N·%d C·%d Q·%d", len(ff1), good, corrupted, mSeq)
s.logf("table@recovery recovered F·%d N·%d C·%d Q·%d", len(tableFiles), good, corrupted, mSeq)
}
// Set sequence number.
rec.setSeq(mSeq + 1)
// Create new manifest.
if err := s.create(); err != nil {
return err
}
// Commit.
return s.commit(rec)
}
func (d *DB) recoverJournal() error {
s := d.s
icmp := s.cmp
ff0, err := s.getFiles(storage.TypeJournal)
func (db *DB) recoverJournal() error {
// Get all tables and sort it by file number.
journalFiles_, err := db.s.getFiles(storage.TypeJournal)
if err != nil {
return err
}
ff1 := files(ff0)
ff1.sort()
ff2 := make([]storage.File, 0, len(ff1))
for _, file := range ff1 {
if file.Num() >= s.stJournalNum || file.Num() == s.stPrevJournalNum {
s.markFileNum(file.Num())
ff2 = append(ff2, file)
journalFiles := files(journalFiles_)
journalFiles.sort()
// Discard older journal.
prev := -1
for i, file := range journalFiles {
if file.Num() >= db.s.stJournalNum {
if prev >= 0 {
i--
journalFiles[i] = journalFiles[prev]
}
journalFiles = journalFiles[i:]
break
} else if file.Num() == db.s.stPrevJournalNum {
prev = i
}
}
@@ -408,38 +440,43 @@ func (d *DB) recoverJournal() error {
var of storage.File
var mem *memdb.DB
batch := new(Batch)
cm := newCMem(s)
cm := newCMem(db.s)
buf := new(util.Buffer)
// Options.
strict := s.o.GetStrict(opt.StrictJournal)
checksum := s.o.GetStrict(opt.StrictJournalChecksum)
writeBuffer := s.o.GetWriteBuffer()
strict := db.s.o.GetStrict(opt.StrictJournal)
checksum := db.s.o.GetStrict(opt.StrictJournalChecksum)
writeBuffer := db.s.o.GetWriteBuffer()
recoverJournal := func(file storage.File) error {
s.logf("journal@recovery recovering @%d", file.Num())
db.logf("journal@recovery recovering @%d", file.Num())
reader, err := file.Open()
if err != nil {
return err
}
defer reader.Close()
// Create/reset journal reader instance.
if jr == nil {
jr = journal.NewReader(reader, dropper{s, file}, strict, checksum)
jr = journal.NewReader(reader, dropper{db.s, file}, strict, checksum)
} else {
jr.Reset(reader, dropper{s, file}, strict, checksum)
jr.Reset(reader, dropper{db.s, file}, strict, checksum)
}
// Flush memdb and remove obsolete journal file.
if of != nil {
if mem.Len() > 0 {
if err := cm.flush(mem, 0); err != nil {
return err
}
}
if err := cm.commit(file.Num(), d.seq); err != nil {
if err := cm.commit(file.Num(), db.seq); err != nil {
return err
}
cm.reset()
of.Remove()
of = nil
}
// Reset memdb.
// Replay journal to memdb.
mem.Reset()
for {
r, err := jr.Next()
@@ -449,12 +486,14 @@ func (d *DB) recoverJournal() error {
}
return err
}
buf.Reset()
if _, err := buf.ReadFrom(r); err != nil {
if strict {
if err == io.ErrUnexpectedEOF {
continue
} else {
return err
}
continue
}
if err := batch.decode(buf.Bytes()); err != nil {
return err
@@ -462,28 +501,37 @@ func (d *DB) recoverJournal() error {
if err := batch.memReplay(mem); err != nil {
return err
}
d.seq = batch.seq + uint64(batch.len())
// Save sequence number.
db.seq = batch.seq + uint64(batch.len())
// Flush it if large enough.
if mem.Size() >= writeBuffer {
// Large enough, flush it.
if err := cm.flush(mem, 0); err != nil {
return err
}
// Reset memdb.
mem.Reset()
}
}
of = file
return nil
}
// Recover all journals.
if len(ff2) > 0 {
s.logf("journal@recovery F·%d", len(ff2))
mem = memdb.New(icmp, writeBuffer)
for _, file := range ff2 {
if len(journalFiles) > 0 {
db.logf("journal@recovery F·%d", len(journalFiles))
// Mark file number as used.
db.s.markFileNum(journalFiles[len(journalFiles)-1].Num())
mem = memdb.New(db.s.icmp, writeBuffer)
for _, file := range journalFiles {
if err := recoverJournal(file); err != nil {
return err
}
}
// Flush the last journal.
if mem.Len() > 0 {
if err := cm.flush(mem, 0); err != nil {
@@ -491,52 +539,60 @@ func (d *DB) recoverJournal() error {
}
}
}
// Create a new journal.
if _, err := d.newMem(0); err != nil {
if _, err := db.newMem(0); err != nil {
return err
}
// Commit.
if err := cm.commit(d.journalFile.Num(), d.seq); err != nil {
if err := cm.commit(db.journalFile.Num(), db.seq); err != nil {
// Close journal.
if db.journal != nil {
db.journal.Close()
db.journalWriter.Close()
}
return err
}
// Remove the last journal.
// Remove the last obsolete journal file.
if of != nil {
of.Remove()
}
return nil
}
func (d *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, err error) {
s := d.s
ucmp := s.cmp.cmp
func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, err error) {
ikey := newIKey(key, seq, tSeek)
em, fm := d.getMems()
for _, m := range [...]*memdb.DB{em, fm} {
em, fm := db.getMems()
for _, m := range [...]*memDB{em, fm} {
if m == nil {
continue
}
mk, mv, me := m.Find(ikey)
defer m.decref()
mk, mv, me := m.mdb.Find(ikey)
if me == nil {
ukey, _, t, ok := parseIkey(mk)
if ok && ucmp.Compare(ukey, key) == 0 {
if ok && db.s.icmp.uCompare(ukey, key) == 0 {
if t == tDel {
return nil, ErrNotFound
}
return mv, nil
return append([]byte{}, mv...), nil
}
} else if me != ErrNotFound {
return nil, me
}
}
v := s.version()
v := db.s.version()
value, cSched, err := v.get(ikey, ro)
v.release()
if cSched {
// Wake compaction.
d.wakeCompaction(0)
// Trigger table compaction.
db.compTrigger(db.tcompTriggerC)
}
return
}
@@ -544,15 +600,16 @@ func (d *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, err
// Get gets the value for the given key. It returns ErrNotFound if the
// DB does not contain the key.
//
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Get returns.
func (d *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
err = d.ok()
// The returned slice is its own copy, it is safe to modify the contents
// of the returned slice.
// It is safe to modify the contents of the argument after Get returns.
func (db *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
err = db.ok()
if err != nil {
return
}
return d.get(key, d.getSeq(), ro)
return db.get(key, db.getSeq(), ro)
}
// NewIterator returns an iterator for the latest snapshot of the
@@ -571,14 +628,14 @@ func (d *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
// The iterator must be released after use, by calling Release method.
//
// Also read Iterator documentation of the leveldb/iterator package.
func (d *DB) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
if err := d.ok(); err != nil {
func (db *DB) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
if err := db.ok(); err != nil {
return iterator.NewEmptyIterator(err)
}
p := d.newSnapshot()
defer p.Release()
return p.NewIterator(slice, ro)
snap := db.newSnapshot()
defer snap.Release()
return snap.NewIterator(slice, ro)
}
// GetSnapshot returns a latest snapshot of the underlying DB. A snapshot
@@ -586,12 +643,12 @@ func (d *DB) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterat
// content of snapshot are guaranteed to be consistent.
//
// The snapshot must be released after use, by calling Release method.
func (d *DB) GetSnapshot() (*Snapshot, error) {
if err := d.ok(); err != nil {
func (db *DB) GetSnapshot() (*Snapshot, error) {
if err := db.ok(); err != nil {
return nil, err
}
return d.newSnapshot(), nil
return db.newSnapshot(), nil
}
// GetProperty returns value of the given property name.
@@ -603,8 +660,18 @@ func (d *DB) GetSnapshot() (*Snapshot, error) {
// Returns statistics of the underlying DB.
// leveldb.sstables
// Returns sstables list for each level.
func (d *DB) GetProperty(name string) (value string, err error) {
err = d.ok()
// leveldb.blockpool
// Returns block pool stats.
// leveldb.cachedblock
// Returns size of cached block.
// leveldb.openedtables
// Returns number of opened tables.
// leveldb.alivesnaps
// Returns number of alive snapshots.
// leveldb.aliveiters
// Returns number of alive iterators.
func (db *DB) GetProperty(name string) (value string, err error) {
err = db.ok()
if err != nil {
return
}
@@ -613,11 +680,9 @@ func (d *DB) GetProperty(name string) (value string, err error) {
if !strings.HasPrefix(name, prefix) {
return "", errors.New("leveldb: GetProperty: unknown property: " + name)
}
p := name[len(prefix):]
s := d.s
v := s.version()
v := db.s.version()
defer v.release()
switch {
@@ -634,22 +699,36 @@ func (d *DB) GetProperty(name string) (value string, err error) {
value = "Compactions\n" +
" Level | Tables | Size(MB) | Time(sec) | Read(MB) | Write(MB)\n" +
"-------+------------+---------------+---------------+---------------+---------------\n"
for level, tt := range v.tables {
duration, read, write := d.compStats[level].get()
if len(tt) == 0 && duration == 0 {
for level, tables := range v.tables {
duration, read, write := db.compStats[level].get()
if len(tables) == 0 && duration == 0 {
continue
}
value += fmt.Sprintf(" %3d | %10d | %13.5f | %13.5f | %13.5f | %13.5f\n",
level, len(tt), float64(tt.size())/1048576.0, duration.Seconds(),
level, len(tables), float64(tables.size())/1048576.0, duration.Seconds(),
float64(read)/1048576.0, float64(write)/1048576.0)
}
case p == "sstables":
for level, tt := range v.tables {
for level, tables := range v.tables {
value += fmt.Sprintf("--- level %d ---\n", level)
for _, t := range tt {
value += fmt.Sprintf("%d:%d[%q .. %q]\n", t.file.Num(), t.size, t.min, t.max)
for _, t := range tables {
value += fmt.Sprintf("%d:%d[%q .. %q]\n", t.file.Num(), t.size, t.imin, t.imax)
}
}
case p == "blockpool":
value = fmt.Sprintf("%v", db.s.tops.bpool)
case p == "cachedblock":
if bc := db.s.o.GetBlockCache(); bc != nil {
value = fmt.Sprintf("%d", bc.Size())
} else {
value = "<nil>"
}
case p == "openedtables":
value = fmt.Sprintf("%d", db.s.tops.cache.Size())
case p == "alivesnaps":
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveSnaps))
case p == "aliveiters":
value = fmt.Sprintf("%d", atomic.LoadInt32(&db.aliveIters))
default:
err = errors.New("leveldb: GetProperty: unknown property: " + name)
}
@@ -657,29 +736,29 @@ func (d *DB) GetProperty(name string) (value string, err error) {
return
}
// GetApproximateSizes calculates approximate sizes of the given key ranges.
// SizeOf calculates approximate sizes of the given key ranges.
// The length of the returned sizes are equal with the length of the given
// ranges. The returned sizes measure storage space usage, so if the user
// data compresses by a factor of ten, the returned sizes will be one-tenth
// the size of the corresponding user data size.
// The results may not include the sizes of recently written data.
func (d *DB) GetApproximateSizes(ranges []util.Range) (Sizes, error) {
if err := d.ok(); err != nil {
func (db *DB) SizeOf(ranges []util.Range) (Sizes, error) {
if err := db.ok(); err != nil {
return nil, err
}
v := d.s.version()
v := db.s.version()
defer v.release()
sizes := make(Sizes, 0, len(ranges))
for _, r := range ranges {
min := newIKey(r.Start, kMaxSeq, tSeek)
max := newIKey(r.Limit, kMaxSeq, tSeek)
start, err := v.getApproximateOffset(min)
imin := newIKey(r.Start, kMaxSeq, tSeek)
imax := newIKey(r.Limit, kMaxSeq, tSeek)
start, err := v.offsetOf(imin)
if err != nil {
return nil, err
}
limit, err := v.getApproximateOffset(max)
limit, err := v.offsetOf(imax)
if err != nil {
return nil, err
}
@@ -693,105 +772,63 @@ func (d *DB) GetApproximateSizes(ranges []util.Range) (Sizes, error) {
return sizes, nil
}
// CompactRange compacts the underlying DB for the given key range.
// In particular, deleted and overwritten versions are discarded,
// and the data is rearranged to reduce the cost of operations
// needed to access the data. This operation should typically only
// be invoked by users who understand the underlying implementation.
//
// A nil Range.Start is treated as a key before all keys in the DB.
// And a nil Range.Limit is treated as a key after all keys in the DB.
// Therefore if both is nil then it will compact entire DB.
func (d *DB) CompactRange(r util.Range) error {
err := d.ok()
if err != nil {
return err
}
cch := make(chan struct{})
req := &cReq{
level: -1,
min: r.Start,
max: r.Limit,
cch: cch,
}
// Push manual compaction request.
select {
case _, _ = <-d.closeCh:
return ErrClosed
case err := <-d.compErrCh:
return err
case d.compReqCh <- req:
}
// Wait for compaction
select {
case _, _ = <-d.closeCh:
return ErrClosed
case <-cch:
}
return nil
}
// Close closes the DB. This will also releases any outstanding snapshot.
// Close closes the DB. This will also releases any outstanding snapshot and
// abort any in-flight compaction.
//
// It is not safe to close a DB until all outstanding iterators are released.
// It is valid to call Close multiple times. Other methods should not be
// called after the DB has been closed.
func (d *DB) Close() error {
if !d.setClosed() {
func (db *DB) Close() error {
if !db.setClosed() {
return ErrClosed
}
s := d.s
start := time.Now()
s.log("db@close closing")
db.log("db@close closing")
// Clear the finalizer.
runtime.SetFinalizer(d, nil)
runtime.SetFinalizer(db, nil)
// Get compaction error.
var err error
select {
case err = <-d.compErrCh:
case err = <-db.compErrC:
default:
}
close(d.closeCh)
close(db.closeC)
// wait for the WaitGroup
d.closeWg.Wait()
// Wait for the close WaitGroup.
db.closeW.Wait()
// close journal
if d.journal != nil {
d.journal.Close()
d.journalWriter.Close()
// Close journal.
db.writeLockC <- struct{}{}
if db.journal != nil {
db.journal.Close()
db.journalWriter.Close()
}
// close session
s.close()
s.logf("db@close done T·%v", time.Since(start))
s.release()
// Close session.
db.s.close()
db.logf("db@close done T·%v", time.Since(start))
db.s.release()
if d.closer != nil {
if err1 := d.closer.Close(); err == nil {
if db.closer != nil {
if err1 := db.closer.Close(); err == nil {
err = err1
}
}
d.s = nil
d.mem = nil
d.frozenMem = nil
d.journal = nil
d.journalWriter = nil
d.journalFile = nil
d.frozenJournalFile = nil
d.snapsRoot = snapshotElement{}
d.closer = nil
close(d.writeCh)
close(d.journalCh)
close(d.compReqCh)
// NIL'ing pointers.
db.s = nil
db.mem = nil
db.frozenMem = nil
db.journal = nil
db.journalWriter = nil
db.journalFile = nil
db.frozenJournalFile = nil
db.snapsRoot = snapshotElement{}
db.closer = nil
return err
}
@@ -15,7 +15,7 @@ import (
)
var (
errTransactExiting = errors.New("leveldb: transact exiting")
errCompactionTransactExiting = errors.New("leveldb: compaction transact exiting")
)
type cStats struct {
@@ -61,12 +61,6 @@ func (p *cStatsStaging) stopTimer() {
}
}
type cReq struct {
level int
min, max iKey
cch chan<- struct{}
}
type cMem struct {
s *session
level int
@@ -80,7 +74,7 @@ func newCMem(s *session) *cMem {
func (c *cMem) flush(mem *memdb.DB, level int) error {
s := c.s
// Write memdb to table
// Write memdb to table.
iter := mem.NewIterator(nil)
defer iter.Release()
t, n, err := s.tops.createFrom(iter)
@@ -88,12 +82,13 @@ func (c *cMem) flush(mem *memdb.DB, level int) error {
return err
}
// Pick level.
if level < 0 {
level = s.version_NB().pickLevel(t.min.ukey(), t.max.ukey())
level = s.version_NB().pickLevel(t.imin.ukey(), t.imax.ukey())
}
c.rec.addTableFile(level, t)
s.logf("mem@flush created L%d@%d N·%d S·%s %q:%q", level, t.file.Num(), n, shortenb(int(t.size)), t.min, t.max)
s.logf("mem@flush created L%d@%d N·%d S·%s %q:%q", level, t.file.Num(), n, shortenb(int(t.size)), t.imin, t.imax)
c.level = level
return nil
@@ -106,95 +101,152 @@ func (c *cMem) reset() {
func (c *cMem) commit(journal, seq uint64) error {
c.rec.setJournalNum(journal)
c.rec.setSeq(seq)
// Commit changes
// Commit changes.
return c.s.commit(c.rec)
}
func (d *DB) compactionError() {
func (db *DB) compactionError() {
var err error
noerr:
for {
select {
case _, _ = <-d.closeCh:
return
case err = <-d.compErrSetCh:
case err = <-db.compErrSetC:
if err != nil {
goto haserr
}
case _, _ = <-db.closeC:
return
}
}
haserr:
for {
select {
case _, _ = <-d.closeCh:
return
case err = <-d.compErrSetCh:
case db.compErrC <- err:
case err = <-db.compErrSetC:
if err == nil {
goto noerr
}
case d.compErrCh <- err:
case _, _ = <-db.closeC:
return
}
}
}
func (d *DB) transact(name string, exec, rollback func() error) {
s := d.s
type compactionTransactCounter int
func (cnt *compactionTransactCounter) incr() {
*cnt++
}
func (db *DB) compactionTransact(name string, exec func(cnt *compactionTransactCounter) error, rollback func() error) {
defer func() {
if x := recover(); x != nil {
if x == errTransactExiting && rollback != nil {
if x == errCompactionTransactExiting && rollback != nil {
if err := rollback(); err != nil {
s.logf("%s rollback error %q", name, err)
db.logf("%s rollback error %q", name, err)
}
}
panic(x)
}
}()
const (
backoffMin = 1 * time.Second
backoffMax = 8 * time.Second
backoffMul = 2 * time.Second
)
backoff := backoffMin
backoffT := time.NewTimer(backoff)
lastCnt := compactionTransactCounter(0)
for n := 0; ; n++ {
if d.isClosed() {
s.logf("%s exiting", name)
panic(errTransactExiting)
// Check wether the DB is closed.
if db.isClosed() {
db.logf("%s exiting", name)
db.compactionExitTransact()
} else if n > 0 {
s.logf("%s retrying N·%d", name, n)
db.logf("%s retrying N·%d", name, n)
}
err := exec()
// Execute.
cnt := compactionTransactCounter(0)
err := exec(&cnt)
// Set compaction error status.
select {
case _, _ = <-d.closeCh:
s.logf("%s exiting", name)
panic(errTransactExiting)
case d.compErrSetCh <- err:
case db.compErrSetC <- err:
case _, _ = <-db.closeC:
db.logf("%s exiting", name)
db.compactionExitTransact()
}
if err == nil {
return
}
s.logf("%s error %q", name, err)
time.Sleep(time.Second)
db.logf("%s error I·%d %q", name, cnt, err)
// Reset backoff duration if counter is advancing.
if cnt > lastCnt {
backoff = backoffMin
lastCnt = cnt
}
// Backoff.
backoffT.Reset(backoff)
if backoff < backoffMax {
backoff *= backoffMul
if backoff > backoffMax {
backoff = backoffMax
}
}
select {
case <-backoffT.C:
case _, _ = <-db.closeC:
db.logf("%s exiting", name)
db.compactionExitTransact()
}
}
}
func (d *DB) memCompaction() {
s := d.s
c := newCMem(s)
stats := new(cStatsStaging)
mem := d.getFrozenMem()
func (db *DB) compactionExitTransact() {
panic(errCompactionTransactExiting)
}
s.logf("mem@flush N·%d S·%s", mem.Len(), shortenb(mem.Size()))
func (db *DB) memCompaction() {
mem := db.getFrozenMem()
if mem == nil {
return
}
defer mem.decref()
c := newCMem(db.s)
stats := new(cStatsStaging)
db.logf("mem@flush N·%d S·%s", mem.mdb.Len(), shortenb(mem.mdb.Size()))
// Don't compact empty memdb.
if mem.Len() == 0 {
s.logf("mem@flush skipping")
if mem.mdb.Len() == 0 {
db.logf("mem@flush skipping")
// drop frozen mem
d.dropFrozenMem()
db.dropFrozenMem()
return
}
d.transact("mem@flush", func() (err error) {
// Pause table compaction.
ch := make(chan struct{})
select {
case db.tcompPauseC <- (chan<- struct{})(ch):
case _, _ = <-db.closeC:
return
}
db.compactionTransact("mem@flush", func(cnt *compactionTransactCounter) (err error) {
stats.startTimer()
defer stats.stopTimer()
return c.flush(mem, -1)
return c.flush(mem.mdb, -1)
}, func() error {
for _, r := range c.rec.addedTables {
s.logf("mem@flush rollback @%d", r.num)
f := s.getTableFile(r.num)
db.logf("mem@flush rollback @%d", r.num)
f := db.s.getTableFile(r.num)
if err := f.Remove(); err != nil {
return err
}
@@ -202,54 +254,59 @@ func (d *DB) memCompaction() {
return nil
})
d.transact("mem@commit", func() (err error) {
db.compactionTransact("mem@commit", func(cnt *compactionTransactCounter) (err error) {
stats.startTimer()
defer stats.stopTimer()
return c.commit(d.journalFile.Num(), d.frozenSeq)
return c.commit(db.journalFile.Num(), db.frozenSeq)
}, nil)
s.logf("mem@flush commited F·%d T·%v", len(c.rec.addedTables), stats.duration)
db.logf("mem@flush committed F·%d T·%v", len(c.rec.addedTables), stats.duration)
for _, r := range c.rec.addedTables {
stats.write += r.size
}
d.compStats[c.level].add(stats)
db.compStats[c.level].add(stats)
// drop frozen mem
d.dropFrozenMem()
// Drop frozen mem.
db.dropFrozenMem()
c = nil
// Resume table compaction.
select {
case <-ch:
case _, _ = <-db.closeC:
return
}
// Trigger table compaction.
db.compTrigger(db.mcompTriggerC)
}
func (d *DB) doCompaction(c *compaction, noTrivial bool) {
s := d.s
ucmp := s.cmp.cmp
func (db *DB) tableCompaction(c *compaction, noTrivial bool) {
rec := new(sessionRecord)
rec.addCompactionPointer(c.level, c.max)
rec.addCompactionPointer(c.level, c.imax)
if !noTrivial && c.trivial() {
t := c.tables[0][0]
s.logf("table@move L%d@%d -> L%d", c.level, t.file.Num(), c.level+1)
db.logf("table@move L%d@%d -> L%d", c.level, t.file.Num(), c.level+1)
rec.deleteTable(c.level, t.file.Num())
rec.addTableFile(c.level+1, t)
d.transact("table@move", func() (err error) {
return s.commit(rec)
db.compactionTransact("table@move", func(cnt *compactionTransactCounter) (err error) {
return db.s.commit(rec)
}, nil)
return
}
var stats [2]cStatsStaging
for i, tt := range c.tables {
for _, t := range tt {
for i, tables := range c.tables {
for _, t := range tables {
stats[i].read += t.size
// Insert deleted tables into record
rec.deleteTable(c.level+i, t.file.Num())
}
}
sourceSize := int(stats[0].read + stats[1].read)
minSeq := d.minSeq()
s.logf("table@compaction L%d·%d -> L%d·%d S·%s Q·%d", c.level, len(c.tables[0]), c.level+1, len(c.tables[1]), shortenb(sourceSize), minSeq)
minSeq := db.minSeq()
db.logf("table@compaction L%d·%d -> L%d·%d S·%s Q·%d", c.level, len(c.tables[0]), c.level+1, len(c.tables[1]), shortenb(sourceSize), minSeq)
var snapUkey []byte
var snapHasUkey bool
@@ -257,7 +314,7 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
var snapIter int
var snapDropCnt int
var dropCnt int
d.transact("table@build", func() (err error) {
db.compactionTransact("table@build", func(cnt *compactionTransactCounter) (err error) {
ukey := append([]byte{}, snapUkey...)
hasUkey := snapHasUkey
lseq := snapSeq
@@ -272,7 +329,7 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
}
rec.addTableFile(c.level+1, t)
stats[1].write += t.size
s.logf("table@build created L%d@%d N·%d S·%s %q:%q", c.level+1, t.file.Num(), tw.tw.EntriesLen(), shortenb(int(t.size)), t.min, t.max)
db.logf("table@build created L%d@%d N·%d S·%s %q:%q", c.level+1, t.file.Num(), tw.tw.EntriesLen(), shortenb(int(t.size)), t.imin, t.imax)
return nil
}
@@ -288,30 +345,17 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
iter := c.newIterator()
defer iter.Release()
for i := 0; iter.Next(); i++ {
// Incr transact counter.
cnt.incr()
// Skip until last state.
if i < snapIter {
continue
}
// Prioritize memdb compaction.
select {
case _, _ = <-d.closeCh:
err = ErrClosed
return
case cch := <-d.compMemCh:
stats[1].stopTimer()
d.memCompaction()
d.compMemAckCh <- struct{}{}
if cch != nil {
cch <- struct{}{}
}
stats[1].startTimer()
default:
}
ikey := iKey(iter.Key())
key := iKey(iter.Key())
if c.shouldStopBefore(key) && tw != nil {
if c.shouldStopBefore(ikey) && tw != nil {
err = finish()
if err != nil {
return
@@ -331,15 +375,15 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
snapSched = false
}
if seq, t, ok := key.parseNum(); !ok {
if seq, vt, ok := ikey.parseNum(); !ok {
// Don't drop error keys
ukey = ukey[:0]
hasUkey = false
lseq = kMaxSeq
} else {
if !hasUkey || ucmp.Compare(key.ukey(), ukey) != 0 {
if !hasUkey || db.s.icmp.uCompare(ikey.ukey(), ukey) != 0 {
// First occurrence of this user key
ukey = append(ukey[:0], key.ukey()...)
ukey = append(ukey[:0], ikey.ukey()...)
hasUkey = true
lseq = kMaxSeq
}
@@ -348,7 +392,7 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
if lseq <= minSeq {
// Dropped because newer entry for same user key exist
drop = true // (A)
} else if t == tDel && seq <= minSeq && c.isBaseLevelForKey(ukey) {
} else if vt == tDel && seq <= minSeq && c.baseLevelForKey(ukey) {
// For this user key:
// (1) there is no data in higher levels
// (2) data in lower levels will have larger seq numbers
@@ -368,14 +412,24 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
// Create new table if not already
if tw == nil {
tw, err = s.tops.create()
// Check for pause event.
select {
case ch := <-db.tcompPauseC:
db.pauseCompaction(ch)
case _, _ = <-db.closeC:
db.compactionExitTransact()
default:
}
// Create new table.
tw, err = db.s.tops.create()
if err != nil {
return
}
}
// Write key/value into table
err = tw.add(key, iter.Value())
err = tw.append(ikey, iter.Value())
if err != nil {
return
}
@@ -407,8 +461,8 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
return
}, func() error {
for _, r := range rec.addedTables {
s.logf("table@build rollback @%d", r.num)
f := s.getTableFile(r.num)
db.logf("table@build rollback @%d", r.num)
f := db.s.getTableFile(r.num)
if err := f.Remove(); err != nil {
return err
}
@@ -417,117 +471,219 @@ func (d *DB) doCompaction(c *compaction, noTrivial bool) {
})
// Commit changes
d.transact("table@commit", func() (err error) {
db.compactionTransact("table@commit", func(cnt *compactionTransactCounter) (err error) {
stats[1].startTimer()
defer stats[1].stopTimer()
return s.commit(rec)
return db.s.commit(rec)
}, nil)
resultSize := int(int(stats[1].write))
s.logf("table@compaction commited F%s S%s D·%d T·%v", sint(len(rec.addedTables)-len(rec.deletedTables)), sshortenb(resultSize-sourceSize), dropCnt, stats[1].duration)
resultSize := int(stats[1].write)
db.logf("table@compaction committed F%s S%s D·%d T·%v", sint(len(rec.addedTables)-len(rec.deletedTables)), sshortenb(resultSize-sourceSize), dropCnt, stats[1].duration)
// Save compaction stats
for i := range stats {
d.compStats[c.level+1].add(&stats[i])
db.compStats[c.level+1].add(&stats[i])
}
}
func (d *DB) compaction() {
s := d.s
func (db *DB) tableRangeCompaction(level int, umin, umax []byte) {
db.logf("table@compaction range L%d %q:%q", level, umin, umax)
if level >= 0 {
if c := db.s.getCompactionRange(level, umin, umax); c != nil {
db.tableCompaction(c, true)
}
} else {
v := db.s.version_NB()
m := 1
for i, t := range v.tables[1:] {
if t.overlaps(db.s.icmp, umin, umax, false) {
m = i + 1
}
}
for level := 0; level < m; level++ {
if c := db.s.getCompactionRange(level, umin, umax); c != nil {
db.tableCompaction(c, true)
}
}
}
}
func (db *DB) tableAutoCompaction() {
if c := db.s.pickCompaction(); c != nil {
db.tableCompaction(c, false)
}
}
func (db *DB) tableNeedCompaction() bool {
return db.s.version_NB().needCompaction()
}
func (db *DB) pauseCompaction(ch chan<- struct{}) {
select {
case ch <- struct{}{}:
case _, _ = <-db.closeC:
db.compactionExitTransact()
}
}
type cCmd interface {
ack(err error)
}
type cIdle struct {
ackC chan<- error
}
func (r cIdle) ack(err error) {
r.ackC <- err
}
type cRange struct {
level int
min, max []byte
ackC chan<- error
}
func (r cRange) ack(err error) {
defer func() {
recover()
}()
if r.ackC != nil {
r.ackC <- err
}
}
func (db *DB) compSendIdle(compC chan<- cCmd) error {
ch := make(chan error)
defer close(ch)
// Send cmd.
select {
case compC <- cIdle{ch}:
case err := <-db.compErrC:
return err
case _, _ = <-db.closeC:
return ErrClosed
}
// Wait cmd.
return <-ch
}
func (db *DB) compSendRange(compC chan<- cCmd, level int, min, max []byte) (err error) {
ch := make(chan error)
defer close(ch)
// Send cmd.
select {
case compC <- cRange{level, min, max, ch}:
case err := <-db.compErrC:
return err
case _, _ = <-db.closeC:
return ErrClosed
}
// Wait cmd.
select {
case err = <-db.compErrC:
case err = <-ch:
}
return err
}
func (db *DB) compTrigger(compTriggerC chan struct{}) {
select {
case compTriggerC <- struct{}{}:
default:
}
}
func (db *DB) mCompaction() {
var x cCmd
defer func() {
if x := recover(); x != nil {
if x != errTransactExiting {
if x != errCompactionTransactExiting {
panic(x)
}
}
d.closeWg.Done()
if x != nil {
x.ack(ErrClosed)
}
db.closeW.Done()
}()
for {
var cch chan<- struct{}
select {
case _, _ = <-d.closeCh:
case x = <-db.mcompCmdC:
db.memCompaction()
x.ack(nil)
x = nil
case <-db.mcompTriggerC:
db.memCompaction()
case _, _ = <-db.closeC:
return
case cch = <-d.compMemCh:
d.memCompaction()
d.compMemAckCh <- struct{}{}
case cch = <-d.compCh:
case creq := <-d.compReqCh:
if creq == nil {
continue
}
s.logf("range compaction L%d %v:%v", creq.level, creq.min, creq.max)
if creq.level >= 0 {
c := s.getCompactionRange(creq.level, creq.min, creq.max)
if c != nil {
d.doCompaction(c, true)
}
} else {
v := s.version_NB()
maxLevel := 1
for i, tt := range v.tables[1:] {
if tt.isOverlaps(creq.min, creq.max, true, s.cmp) {
maxLevel = i + 1
}
}
for i := 0; i < maxLevel; i++ {
c := s.getCompactionRange(i, creq.min, creq.max)
if c != nil {
d.doCompaction(c, true)
}
}
}
cch = creq.cch
}
if s.version_NB().needCompaction() {
d.doCompaction(s.pickCompaction(), false)
select {
case d.compCh <- nil:
default:
}
}
if cch != nil {
func() {
defer func() {
recover()
}()
cch <- struct{}{}
}()
}
}
}
func (d *DB) wakeCompaction(wait int) error {
switch wait {
case 0:
select {
case d.compCh <- nil:
default:
func (db *DB) tCompaction() {
var x cCmd
var ackQ []cCmd
defer func() {
if x := recover(); x != nil {
if x != errCompactionTransactExiting {
panic(x)
}
}
case 1:
select {
case _, _ = <-d.closeCh:
return ErrClosed
case err := <-d.compErrCh:
return err
case d.compCh <- nil:
for i := range ackQ {
ackQ[i].ack(ErrClosed)
ackQ[i] = nil
}
case 2:
cch := make(chan struct{})
defer close(cch)
select {
case _, _ = <-d.closeCh:
return ErrClosed
case err := <-d.compErrCh:
return err
case d.compCh <- (chan<- struct{})(cch):
if x != nil {
x.ack(ErrClosed)
}
select {
case _, _ = <-d.closeCh:
return ErrClosed
case err := <-d.compErrCh:
return err
case <-cch:
db.closeW.Done()
}()
for {
if db.tableNeedCompaction() {
select {
case x = <-db.tcompCmdC:
case <-db.tcompTriggerC:
case ch := <-db.tcompPauseC:
db.pauseCompaction(ch)
continue
case _, _ = <-db.closeC:
return
default:
}
} else {
for i := range ackQ {
ackQ[i].ack(nil)
ackQ[i] = nil
}
ackQ = ackQ[:0]
select {
case x = <-db.tcompCmdC:
case <-db.tcompTriggerC:
case ch := <-db.tcompPauseC:
db.pauseCompaction(ch)
continue
case _, _ = <-db.closeC:
return
}
}
if x != nil {
switch cmd := x.(type) {
case cIdle:
ackQ = append(ackQ, x)
case cRange:
db.tableRangeCompaction(cmd.level, cmd.min, cmd.max)
x.ack(nil)
}
x = nil
}
db.tableAutoCompaction()
}
return nil
}
@@ -9,8 +9,9 @@ package leveldb
import (
"errors"
"runtime"
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb/comparer"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/util"
@@ -20,46 +21,61 @@ var (
errInvalidIkey = errors.New("leveldb: Iterator: invalid internal key")
)
func (db *DB) newRawIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
s := db.s
type memdbReleaser struct {
once sync.Once
m *memDB
}
func (mr *memdbReleaser) Release() {
mr.once.Do(func() {
mr.m.decref()
})
}
func (db *DB) newRawIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
em, fm := db.getMems()
v := s.version()
v := db.s.version()
ti := v.getIterators(slice, ro)
n := len(ti) + 2
i := make([]iterator.Iterator, 0, n)
i = append(i, em.NewIterator(slice))
emi := em.mdb.NewIterator(slice)
emi.SetReleaser(&memdbReleaser{m: em})
i = append(i, emi)
if fm != nil {
i = append(i, fm.NewIterator(slice))
fmi := fm.mdb.NewIterator(slice)
fmi.SetReleaser(&memdbReleaser{m: fm})
i = append(i, fmi)
}
i = append(i, ti...)
strict := s.o.GetStrict(opt.StrictIterator) || ro.GetStrict(opt.StrictIterator)
mi := iterator.NewMergedIterator(i, s.cmp, strict)
strict := db.s.o.GetStrict(opt.StrictIterator) || ro.GetStrict(opt.StrictIterator)
mi := iterator.NewMergedIterator(i, db.s.icmp, strict)
mi.SetReleaser(&versionReleaser{v: v})
return mi
}
func (db *DB) newIterator(seq uint64, slice *util.Range, ro *opt.ReadOptions) *dbIter {
var slice_ *util.Range
var islice *util.Range
if slice != nil {
slice_ = &util.Range{}
islice = &util.Range{}
if slice.Start != nil {
slice_.Start = newIKey(slice.Start, kMaxSeq, tSeek)
islice.Start = newIKey(slice.Start, kMaxSeq, tSeek)
}
if slice.Limit != nil {
slice_.Limit = newIKey(slice.Limit, kMaxSeq, tSeek)
islice.Limit = newIKey(slice.Limit, kMaxSeq, tSeek)
}
}
rawIter := db.newRawIterator(slice_, ro)
rawIter := db.newRawIterator(islice, ro)
iter := &dbIter{
cmp: db.s.cmp.cmp,
db: db,
icmp: db.s.icmp,
iter: rawIter,
seq: seq,
strict: db.s.o.GetStrict(opt.StrictIterator) || ro.GetStrict(opt.StrictIterator),
key: make([]byte, 0),
value: make([]byte, 0),
}
atomic.AddInt32(&db.aliveIters, 1)
runtime.SetFinalizer(iter, (*dbIter).Release)
return iter
}
@@ -76,7 +92,8 @@ const (
// dbIter represent an interator states over a database session.
type dbIter struct {
cmp comparer.BasicComparer
db *DB
icmp *iComparer
iter iterator.Iterator
seq uint64
strict bool
@@ -166,7 +183,7 @@ func (i *dbIter) next() bool {
i.key = append(i.key[:0], ukey...)
i.dir = dirForward
case tVal:
if i.dir == dirSOI || i.cmp.Compare(ukey, i.key) > 0 {
if i.dir == dirSOI || i.icmp.uCompare(ukey, i.key) > 0 {
i.key = append(i.key[:0], ukey...)
i.value = append(i.value[:0], i.iter.Value()...)
i.dir = dirForward
@@ -211,7 +228,7 @@ func (i *dbIter) prev() bool {
ukey, seq, t, ok := parseIkey(i.iter.Key())
if ok {
if seq <= i.seq {
if !del && i.cmp.Compare(ukey, i.key) < 0 {
if !del && i.icmp.uCompare(ukey, i.key) < 0 {
return true
}
del = (t == tDel)
@@ -252,7 +269,7 @@ func (i *dbIter) Prev() bool {
for i.iter.Prev() {
ukey, _, _, ok := parseIkey(i.iter.Key())
if ok {
if i.cmp.Compare(ukey, i.key) < 0 {
if i.icmp.uCompare(ukey, i.key) < 0 {
goto cont
}
} else if i.strict {
@@ -290,6 +307,7 @@ func (i *dbIter) Release() {
if i.releaser != nil {
i.releaser.Release()
i.releaser = nil
}
i.dir = dirReleased
@@ -297,13 +315,19 @@ func (i *dbIter) Release() {
i.value = nil
i.iter.Release()
i.iter = nil
atomic.AddInt32(&i.db.aliveIters, -1)
i.db = nil
}
}
func (i *dbIter) SetReleaser(releaser util.Releaser) {
if i.dir != dirReleased {
i.releaser = releaser
if i.dir == dirReleased {
panic(util.ErrReleased)
}
if i.releaser != nil && releaser != nil {
panic(util.ErrHasReleaser)
}
i.releaser = releaser
}
func (i *dbIter) Error() error {
@@ -9,6 +9,7 @@ package leveldb
import (
"runtime"
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
@@ -81,18 +82,19 @@ func (db *DB) minSeq() uint64 {
type Snapshot struct {
db *DB
elem *snapshotElement
mu sync.Mutex
mu sync.RWMutex
released bool
}
// Creates new snapshot object.
func (db *DB) newSnapshot() *Snapshot {
p := &Snapshot{
snap := &Snapshot{
db: db,
elem: db.acquireSnapshot(),
}
runtime.SetFinalizer(p, (*Snapshot).Release)
return p
atomic.AddInt32(&db.aliveSnaps, 1)
runtime.SetFinalizer(snap, (*Snapshot).Release)
return snap
}
// Get gets the value for the given key. It returns ErrNotFound if
@@ -100,19 +102,18 @@ func (db *DB) newSnapshot() *Snapshot {
//
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Get returns.
func (p *Snapshot) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
db := p.db
err = db.ok()
func (snap *Snapshot) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
err = snap.db.ok()
if err != nil {
return
}
p.mu.Lock()
defer p.mu.Unlock()
if p.released {
snap.mu.RLock()
defer snap.mu.RUnlock()
if snap.released {
err = ErrSnapshotReleased
return
}
return db.get(key, p.elem.seq, ro)
return snap.db.get(key, snap.elem.seq, ro)
}
// NewIterator returns an iterator for the snapshot of the uderlying DB.
@@ -132,17 +133,18 @@ func (p *Snapshot) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error
// iterator would be still valid until released.
//
// Also read Iterator documentation of the leveldb/iterator package.
func (p *Snapshot) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
db := p.db
if err := db.ok(); err != nil {
func (snap *Snapshot) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
if err := snap.db.ok(); err != nil {
return iterator.NewEmptyIterator(err)
}
p.mu.Lock()
defer p.mu.Unlock()
if p.released {
snap.mu.Lock()
defer snap.mu.Unlock()
if snap.released {
return iterator.NewEmptyIterator(ErrSnapshotReleased)
}
return db.newIterator(p.elem.seq, slice, ro)
// Since iterator already hold version ref, it doesn't need to
// hold snapshot ref.
return snap.db.newIterator(snap.elem.seq, slice, ro)
}
// Release releases the snapshot. This will not release any returned
@@ -150,16 +152,18 @@ func (p *Snapshot) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.
// underlying DB is closed.
//
// Other methods should not be called after the snapshot has been released.
func (p *Snapshot) Release() {
p.mu.Lock()
if !p.released {
// Clear the finalizer.
runtime.SetFinalizer(p, nil)
func (snap *Snapshot) Release() {
snap.mu.Lock()
defer snap.mu.Unlock()
p.released = true
p.db.releaseSnapshot(p.elem)
p.db = nil
p.elem = nil
if !snap.released {
// Clear the finalizer.
runtime.SetFinalizer(snap, nil)
snap.released = true
snap.db.releaseSnapshot(snap.elem)
atomic.AddInt32(&snap.db.aliveSnaps, -1)
snap.db = nil
snap.elem = nil
}
p.mu.Unlock()
}
@@ -8,106 +8,194 @@ package leveldb
import (
"sync/atomic"
"time"
"github.com/syndtr/goleveldb/leveldb/journal"
"github.com/syndtr/goleveldb/leveldb/memdb"
)
type memDB struct {
db *DB
mdb *memdb.DB
ref int32
}
func (m *memDB) incref() {
atomic.AddInt32(&m.ref, 1)
}
func (m *memDB) decref() {
if ref := atomic.AddInt32(&m.ref, -1); ref == 0 {
// Only put back memdb with std capacity.
if m.mdb.Capacity() == m.db.s.o.GetWriteBuffer() {
m.mdb.Reset()
m.db.mpoolPut(m.mdb)
}
m.db = nil
m.mdb = nil
} else if ref < 0 {
panic("negative memdb ref")
}
}
// Get latest sequence number.
func (d *DB) getSeq() uint64 {
return atomic.LoadUint64(&d.seq)
func (db *DB) getSeq() uint64 {
return atomic.LoadUint64(&db.seq)
}
// Atomically adds delta to seq.
func (d *DB) addSeq(delta uint64) {
atomic.AddUint64(&d.seq, delta)
func (db *DB) addSeq(delta uint64) {
atomic.AddUint64(&db.seq, delta)
}
func (db *DB) mpoolPut(mem *memdb.DB) {
defer func() {
recover()
}()
select {
case db.memPool <- mem:
default:
}
}
func (db *DB) mpoolGet() *memdb.DB {
select {
case mem := <-db.memPool:
return mem
default:
return nil
}
}
func (db *DB) mpoolDrain() {
ticker := time.NewTicker(30 * time.Second)
for {
select {
case <-ticker.C:
select {
case <-db.memPool:
default:
}
case _, _ = <-db.closeC:
close(db.memPool)
return
}
}
}
// Create new memdb and froze the old one; need external synchronization.
// newMem only called synchronously by the writer.
func (d *DB) newMem(n int) (mem *memdb.DB, err error) {
s := d.s
num := s.allocFileNum()
file := s.getJournalFile(num)
func (db *DB) newMem(n int) (mem *memDB, err error) {
num := db.s.allocFileNum()
file := db.s.getJournalFile(num)
w, err := file.Create()
if err != nil {
s.reuseFileNum(num)
db.s.reuseFileNum(num)
return
}
d.memMu.Lock()
if d.journal == nil {
d.journal = journal.NewWriter(w)
} else {
d.journal.Reset(w)
d.journalWriter.Close()
d.frozenJournalFile = d.journalFile
db.memMu.Lock()
defer db.memMu.Unlock()
if db.frozenMem != nil {
panic("still has frozen mem")
}
d.journalWriter = w
d.journalFile = file
d.frozenMem = d.mem
d.mem = memdb.New(s.cmp, maxInt(d.s.o.GetWriteBuffer(), n))
mem = d.mem
// The seq only incremented by the writer.
d.frozenSeq = d.seq
d.memMu.Unlock()
if db.journal == nil {
db.journal = journal.NewWriter(w)
} else {
db.journal.Reset(w)
db.journalWriter.Close()
db.frozenJournalFile = db.journalFile
}
db.journalWriter = w
db.journalFile = file
db.frozenMem = db.mem
mdb := db.mpoolGet()
if mdb == nil || mdb.Capacity() < n {
mdb = memdb.New(db.s.icmp, maxInt(db.s.o.GetWriteBuffer(), n))
}
mem = &memDB{
db: db,
mdb: mdb,
ref: 2,
}
db.mem = mem
// The seq only incremented by the writer. And whoever called newMem
// should hold write lock, so no need additional synchronization here.
db.frozenSeq = db.seq
return
}
// Get all memdbs.
func (d *DB) getMems() (e *memdb.DB, f *memdb.DB) {
d.memMu.RLock()
defer d.memMu.RUnlock()
return d.mem, d.frozenMem
func (db *DB) getMems() (e, f *memDB) {
db.memMu.RLock()
defer db.memMu.RUnlock()
if db.mem == nil {
panic("nil effective mem")
}
db.mem.incref()
if db.frozenMem != nil {
db.frozenMem.incref()
}
return db.mem, db.frozenMem
}
// Get frozen memdb.
func (d *DB) getEffectiveMem() *memdb.DB {
d.memMu.RLock()
defer d.memMu.RUnlock()
return d.mem
func (db *DB) getEffectiveMem() *memDB {
db.memMu.RLock()
defer db.memMu.RUnlock()
if db.mem == nil {
panic("nil effective mem")
}
db.mem.incref()
return db.mem
}
// Check whether we has frozen memdb.
func (d *DB) hasFrozenMem() bool {
d.memMu.RLock()
defer d.memMu.RUnlock()
return d.frozenMem != nil
func (db *DB) hasFrozenMem() bool {
db.memMu.RLock()
defer db.memMu.RUnlock()
return db.frozenMem != nil
}
// Get frozen memdb.
func (d *DB) getFrozenMem() *memdb.DB {
d.memMu.RLock()
defer d.memMu.RUnlock()
return d.frozenMem
func (db *DB) getFrozenMem() *memDB {
db.memMu.RLock()
defer db.memMu.RUnlock()
if db.frozenMem != nil {
db.frozenMem.incref()
}
return db.frozenMem
}
// Drop frozen memdb; assume that frozen memdb isn't nil.
func (d *DB) dropFrozenMem() {
d.memMu.Lock()
if err := d.frozenJournalFile.Remove(); err != nil {
d.s.logf("journal@remove removing @%d %q", d.frozenJournalFile.Num(), err)
func (db *DB) dropFrozenMem() {
db.memMu.Lock()
if err := db.frozenJournalFile.Remove(); err != nil {
db.logf("journal@remove removing @%d %q", db.frozenJournalFile.Num(), err)
} else {
d.s.logf("journal@remove removed @%d", d.frozenJournalFile.Num())
db.logf("journal@remove removed @%d", db.frozenJournalFile.Num())
}
d.frozenJournalFile = nil
d.frozenMem = nil
d.memMu.Unlock()
db.frozenJournalFile = nil
db.frozenMem.decref()
db.frozenMem = nil
db.memMu.Unlock()
}
// Set closed flag; return true if not already closed.
func (d *DB) setClosed() bool {
return atomic.CompareAndSwapUint32(&d.closed, 0, 1)
func (db *DB) setClosed() bool {
return atomic.CompareAndSwapUint32(&db.closed, 0, 1)
}
// Check whether DB was closed.
func (d *DB) isClosed() bool {
return atomic.LoadUint32(&d.closed) != 0
func (db *DB) isClosed() bool {
return atomic.LoadUint32(&db.closed) != 0
}
// Check read ok status.
func (d *DB) ok() error {
if d.isClosed() {
func (db *DB) ok() error {
if db.isClosed() {
return ErrClosed
}
return nil
@@ -124,13 +124,15 @@ func (h *dbHarness) openAssert(want bool) {
}
}
func (h *dbHarness) put(key, value string) {
t := h.t
db := h.db
func (h *dbHarness) write(batch *Batch) {
if err := h.db.Write(batch, h.wo); err != nil {
h.t.Error("Write: got error: ", err)
}
}
err := db.Put([]byte(key), []byte(value), h.wo)
if err != nil {
t.Error("Put: got error: ", err)
func (h *dbHarness) put(key, value string) {
if err := h.db.Put([]byte(key), []byte(value), h.wo); err != nil {
h.t.Error("Put: got error: ", err)
}
}
@@ -147,15 +149,12 @@ func (h *dbHarness) maxNextLevelOverlappingBytes(want uint64) {
db := h.db
var res uint64
ucmp := db.s.cmp.cmp
v := db.s.version()
for i, tt := range v.tables[1 : len(v.tables)-1] {
level := i + 1
next := v.tables[level+1]
for _, t := range tt {
var r tFiles
min, max := t.min.ukey(), t.max.ukey()
next.getOverlaps(min, max, &r, true, ucmp)
r := next.getOverlaps(nil, db.s.icmp, t.imin.ukey(), t.imax.ukey(), false)
sum := r.size()
if sum > res {
res = sum
@@ -179,6 +178,21 @@ func (h *dbHarness) delete(key string) {
}
}
func (h *dbHarness) assertNumKeys(want int) {
iter := h.db.NewIterator(nil, h.ro)
defer iter.Release()
got := 0
for iter.Next() {
got++
}
if err := iter.Error(); err != nil {
h.t.Error("assertNumKeys: ", err)
}
if want != got {
h.t.Errorf("assertNumKeys: want=%d got=%d", want, got)
}
}
func (h *dbHarness) getr(db Reader, key string, expectFound bool) (found bool, v []byte) {
t := h.t
v, err := db.Get([]byte(key), h.ro)
@@ -221,7 +235,7 @@ func (h *dbHarness) getVal(key, value string) {
func (h *dbHarness) allEntriesFor(key, want string) {
t := h.t
db := h.db
ucmp := db.s.cmp.cmp
s := db.s
ikey := newIKey([]byte(key), kMaxSeq, tVal)
iter := db.newRawIterator(nil, nil)
@@ -234,7 +248,7 @@ func (h *dbHarness) allEntriesFor(key, want string) {
for iter.Valid() {
rkey := iKey(iter.Key())
if _, t, ok := rkey.parseNum(); ok {
if ucmp.Compare(ikey.ukey(), rkey.ukey()) != 0 {
if s.icmp.uCompare(ikey.ukey(), rkey.ukey()) != 0 {
break
}
if !first {
@@ -291,7 +305,16 @@ func (h *dbHarness) getKeyVal(want string) {
func (h *dbHarness) waitCompaction() {
t := h.t
db := h.db
if err := db.wakeCompaction(2); err != nil {
if err := db.compSendIdle(db.tcompCmdC); err != nil {
t.Error("compaction error: ", err)
}
}
func (h *dbHarness) waitMemCompaction() {
t := h.t
db := h.db
if err := db.compSendIdle(db.mcompCmdC); err != nil {
t.Error("compaction error: ", err)
}
}
@@ -300,41 +323,15 @@ func (h *dbHarness) compactMem() {
t := h.t
db := h.db
if err := db.wakeCompaction(1); err != nil {
db.writeLockC <- struct{}{}
defer func() {
<-db.writeLockC
}()
if _, err := db.rotateMem(0); err != nil {
t.Error("compaction error: ", err)
return
}
if mem := db.getEffectiveMem(); mem.Len() == 0 {
return
}
select {
case <-db.compMemAckCh:
case err := <-db.compErrCh:
t.Error("compaction error: ", err)
return
}
// create new memdb and journal
_, err := db.newMem(0)
if err != nil {
t.Error("newMem: got error: ", err)
return
}
cch := make(chan struct{})
// Schedule mem compaction.
select {
case db.compMemCh <- (chan<- struct{})(cch):
case err := <-db.compErrCh:
t.Error("compaction error: ", err)
return
}
// Wait.
select {
case <-cch:
case err := <-db.compErrCh:
if err := db.compSendIdle(db.mcompCmdC); err != nil {
t.Error("compaction error: ", err)
}
@@ -347,35 +344,22 @@ func (h *dbHarness) compactRangeAtErr(level int, min, max string, wanterr bool)
t := h.t
db := h.db
cch := make(chan struct{})
req := &cReq{level: level, cch: cch}
var _min, _max []byte
if min != "" {
req.min = []byte(min)
_min = []byte(min)
}
if max != "" {
req.max = []byte(max)
_max = []byte(max)
}
// Push manual compaction request.
select {
case err := <-db.compErrCh:
t.Error("CompactRangeAt: compaction error: ", err)
return
case db.compReqCh <- req:
}
// Wait for compaction
select {
case err := <-db.compErrCh:
if err := db.compSendRange(db.tcompCmdC, level, _min, _max); err != nil {
if wanterr {
t.Log("CompactRangeAt: got error (expected): ", err)
} else {
t.Error("CompactRangeAt: got error: ", err)
}
case <-cch:
if wanterr {
t.Error("CompactRangeAt: expect error")
}
} else if wanterr {
t.Error("CompactRangeAt: expect error")
}
}
@@ -394,8 +378,7 @@ func (h *dbHarness) compactRange(min, max string) {
if max != "" {
r.Limit = []byte(max)
}
err := db.CompactRange(r)
if err != nil {
if err := db.CompactRange(r); err != nil {
t.Error("CompactRange: got error: ", err)
}
}
@@ -404,11 +387,11 @@ func (h *dbHarness) sizeAssert(start, limit string, low, hi uint64) {
t := h.t
db := h.db
s, err := db.GetApproximateSizes([]util.Range{
s, err := db.SizeOf([]util.Range{
{[]byte(start), []byte(limit)},
})
if err != nil {
t.Error("GetApproximateSizes: got error: ", err)
t.Error("SizeOf: got error: ", err)
}
if s.Sum() < low || s.Sum() > hi {
t.Errorf("sizeof %q to %q not in range, want %d - %d, got %d",
@@ -1008,6 +991,7 @@ func TestDb_SparseMerge(t *testing.T) {
h.put("C", "vc")
h.compactMem()
h.compactRangeAt(0, "", "")
h.waitCompaction()
// Make sparse update
h.put("A", "va2")
@@ -1017,12 +1001,14 @@ func TestDb_SparseMerge(t *testing.T) {
h.maxNextLevelOverlappingBytes(20 * 1048576)
h.compactRangeAt(0, "", "")
h.waitCompaction()
h.maxNextLevelOverlappingBytes(20 * 1048576)
h.compactRangeAt(1, "", "")
h.waitCompaction()
h.maxNextLevelOverlappingBytes(20 * 1048576)
}
func TestDb_ApproximateSizes(t *testing.T) {
func TestDb_SizeOf(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
Compression: opt.NoCompression,
WriteBuffer: 10000000,
@@ -1042,7 +1028,7 @@ func TestDb_ApproximateSizes(t *testing.T) {
h.put(numKey(i), strings.Repeat(fmt.Sprintf("v%09d", i), s1/10))
}
// 0 because GetApproximateSizes() does not account for memtable space
// 0 because SizeOf() does not account for memtable space
h.sizeAssert("", numKey(50), 0, 0)
for r := 0; r < 3; r++ {
@@ -1072,7 +1058,7 @@ func TestDb_ApproximateSizes(t *testing.T) {
}
}
func TestDb_ApproximateSizes_MixOfSmallAndLarge(t *testing.T) {
func TestDb_SizeOf_MixOfSmallAndLarge(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{Compression: opt.NoCompression})
defer h.close()
@@ -1224,7 +1210,7 @@ func TestDb_DeletionMarkers2(t *testing.T) {
}
func TestDb_CompactionTableOpenError(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{MaxOpenFiles: 0})
h := newDbHarnessWopt(t, &opt.Options{CachedOpenFiles: -1})
defer h.close()
im := 10
@@ -1244,7 +1230,7 @@ func TestDb_CompactionTableOpenError(t *testing.T) {
h.stor.SetOpenErr(storage.TypeTable)
go h.db.CompactRange(util.Range{})
if err := h.db.wakeCompaction(2); err != nil {
if err := h.db.compSendIdle(h.db.tcompCmdC); err != nil {
t.Log("compaction error: ", err)
}
h.closeDB0()
@@ -1486,7 +1472,7 @@ func TestDb_ClosedIsClosed(t *testing.T) {
_, err = db.GetProperty("leveldb.stats")
assertErr(t, err, true)
_, err = db.GetApproximateSizes([]util.Range{{[]byte("a"), []byte("z")}})
_, err = db.SizeOf([]util.Range{{[]byte("a"), []byte("z")}})
assertErr(t, err, true)
assertErr(t, db.CompactRange(util.Range{}), true)
@@ -1591,7 +1577,7 @@ func TestDb_BloomFilter(t *testing.T) {
return fmt.Sprintf("key%06d", i)
}
n := 10000
const n = 10000
// Populate multiple layers
for i := 0; i < n; i++ {
@@ -1828,3 +1814,73 @@ func TestDb_DeletionMarkersOnMemdb(t *testing.T) {
h.get("foo", false)
h.getKeyVal("")
}
func TestDb_LeveldbIssue178(t *testing.T) {
nKeys := (kMaxTableSize / 30) * 5
key1 := func(i int) string {
return fmt.Sprintf("my_key_%d", i)
}
key2 := func(i int) string {
return fmt.Sprintf("my_key_%d_xxx", i)
}
// Disable compression since it affects the creation of layers and the
// code below is trying to test against a very specific scenario.
h := newDbHarnessWopt(t, &opt.Options{Compression: opt.NoCompression})
defer h.close()
// Create first key range.
batch := new(Batch)
for i := 0; i < nKeys; i++ {
batch.Put([]byte(key1(i)), []byte("value for range 1 key"))
}
h.write(batch)
// Create second key range.
batch.Reset()
for i := 0; i < nKeys; i++ {
batch.Put([]byte(key2(i)), []byte("value for range 2 key"))
}
h.write(batch)
// Delete second key range.
batch.Reset()
for i := 0; i < nKeys; i++ {
batch.Delete([]byte(key2(i)))
}
h.write(batch)
h.waitMemCompaction()
// Run manual compaction.
h.compactRange(key1(0), key1(nKeys-1))
// Checking the keys.
h.assertNumKeys(nKeys)
}
func TestDb_LeveldbIssue200(t *testing.T) {
h := newDbHarness(t)
defer h.close()
h.put("1", "b")
h.put("2", "c")
h.put("3", "d")
h.put("4", "e")
h.put("5", "f")
iter := h.db.NewIterator(nil, h.ro)
// Add an element that should not be reflected in the iterator.
h.put("25", "cd")
iter.Seek([]byte("5"))
assertBytes(t, []byte("5"), iter.Key())
iter.Prev()
assertBytes(t, []byte("4"), iter.Key())
iter.Prev()
assertBytes(t, []byte("3"), iter.Key())
iter.Next()
assertBytes(t, []byte("4"), iter.Key())
iter.Next()
assertBytes(t, []byte("5"), iter.Key())
}
@@ -7,6 +7,8 @@
package leveldb
import (
"errors"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/storage"
@@ -30,45 +32,63 @@ func (p Sizes) Sum() (n uint64) {
return n
}
// Remove unused files.
func (d *DB) cleanFiles() error {
s := d.s
// Logging.
func (db *DB) log(v ...interface{}) { db.s.log(v...) }
func (db *DB) logf(format string, v ...interface{}) { db.s.logf(format, v...) }
v := s.version_NB()
tables := make(map[uint64]struct{})
for _, tt := range v.tables {
for _, t := range tt {
tables[t.file.Num()] = struct{}{}
// Check and clean files.
func (db *DB) checkAndCleanFiles() error {
v := db.s.version_NB()
tablesMap := make(map[uint64]bool)
for _, tables := range v.tables {
for _, t := range tables {
tablesMap[t.file.Num()] = false
}
}
ff, err := s.getFiles(storage.TypeAll)
files, err := db.s.getFiles(storage.TypeAll)
if err != nil {
return err
}
var nTables int
var rem []storage.File
for _, f := range ff {
for _, f := range files {
keep := true
switch f.Type() {
case storage.TypeManifest:
keep = f.Num() >= s.manifestFile.Num()
keep = f.Num() >= db.s.manifestFile.Num()
case storage.TypeJournal:
if d.frozenJournalFile != nil {
keep = f.Num() >= d.frozenJournalFile.Num()
if db.frozenJournalFile != nil {
keep = f.Num() >= db.frozenJournalFile.Num()
} else {
keep = f.Num() >= d.journalFile.Num()
keep = f.Num() >= db.journalFile.Num()
}
case storage.TypeTable:
_, keep = tables[f.Num()]
_, keep = tablesMap[f.Num()]
if keep {
tablesMap[f.Num()] = true
nTables++
}
}
if !keep {
rem = append(rem, f)
}
}
s.logf("db@janitor F·%d G·%d", len(ff), len(rem))
if nTables != len(tablesMap) {
for num, present := range tablesMap {
if !present {
db.logf("db@janitor table missing @%d", num)
}
}
return ErrCorrupted{Type: MissingFiles, Err: errors.New("leveldb: table files missing")}
}
db.logf("db@janitor F·%d G·%d", len(files), len(rem))
for _, f := range rem {
s.logf("db@janitor removing %s-%d", f.Type(), f.Num())
db.logf("db@janitor removing %s-%d", f.Type(), f.Num())
if err := f.Remove(); err != nil {
return err
}
@@ -11,48 +11,71 @@ import (
"github.com/syndtr/goleveldb/leveldb/memdb"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/util"
)
func (d *DB) doWriteJournal(b *Batch) error {
w, err := d.journal.Next()
func (db *DB) writeJournal(b *Batch) error {
w, err := db.journal.Next()
if err != nil {
return err
}
if _, err := w.Write(b.encode()); err != nil {
return err
}
if err := d.journal.Flush(); err != nil {
if err := db.journal.Flush(); err != nil {
return err
}
if b.sync {
return d.journalWriter.Sync()
return db.journalWriter.Sync()
}
return nil
}
func (d *DB) writeJournal() {
defer d.closeWg.Done()
func (db *DB) jWriter() {
defer db.closeW.Done()
for {
select {
case _, _ = <-d.closeCh:
return
case b := <-d.journalCh:
case b := <-db.journalC:
if b != nil {
d.journalAckCh <- d.doWriteJournal(b)
db.journalAckC <- db.writeJournal(b)
}
case _, _ = <-db.closeC:
return
}
}
}
func (d *DB) flush(n int) (mem *memdb.DB, nn int, err error) {
s := d.s
func (db *DB) rotateMem(n int) (mem *memDB, err error) {
// Wait for pending memdb compaction.
err = db.compSendIdle(db.mcompCmdC)
if err != nil {
return
}
// Create new memdb and journal.
mem, err = db.newMem(n)
if err != nil {
return
}
// Schedule memdb compaction.
db.compTrigger(db.mcompTriggerC)
return
}
func (db *DB) flush(n int) (mem *memDB, nn int, err error) {
delayed := false
flush := func() bool {
v := s.version()
flush := func() (retry bool) {
v := db.s.version()
defer v.release()
mem = d.getEffectiveMem()
nn = mem.Free()
mem = db.getEffectiveMem()
defer func() {
if retry {
mem.decref()
mem = nil
}
}()
nn = mem.mdb.Free()
switch {
case v.tLen(0) >= kL0_SlowdownWritesTrigger && !delayed:
delayed = true
@@ -61,33 +84,23 @@ func (d *DB) flush(n int) (mem *memdb.DB, nn int, err error) {
return false
case v.tLen(0) >= kL0_StopWritesTrigger:
delayed = true
err = d.wakeCompaction(2)
err = db.compSendIdle(db.tcompCmdC)
if err != nil {
return false
}
default:
// Allow memdb to grow if it has no entry.
if mem.Len() == 0 {
if mem.mdb.Len() == 0 {
nn = n
return false
} else {
mem.decref()
mem, err = db.rotateMem(n)
if err == nil {
nn = mem.mdb.Free()
} else {
nn = 0
}
}
// Wait for pending memdb compaction.
select {
case _, _ = <-d.closeCh:
err = ErrClosed
return false
case <-d.compMemAckCh:
case err = <-d.compErrCh:
return false
}
// Create new memdb and journal.
mem, err = d.newMem(n)
if err != nil {
return false
}
// Schedule memdb compaction.
d.compMemCh <- nil
return false
}
return true
@@ -96,7 +109,7 @@ func (d *DB) flush(n int) (mem *memdb.DB, nn int, err error) {
for flush() {
}
if delayed {
s.logf("db@write delayed T·%v", time.Since(start))
db.logf("db@write delayed T·%v", time.Since(start))
}
return
}
@@ -105,8 +118,8 @@ func (d *DB) flush(n int) (mem *memdb.DB, nn int, err error) {
// sequentially.
//
// It is safe to modify the contents of the arguments after Write returns.
func (d *DB) Write(b *Batch, wo *opt.WriteOptions) (err error) {
err = d.ok()
func (db *DB) Write(b *Batch, wo *opt.WriteOptions) (err error) {
err = db.ok()
if err != nil || b == nil || b.len() == 0 {
return
}
@@ -116,28 +129,29 @@ func (d *DB) Write(b *Batch, wo *opt.WriteOptions) (err error) {
// The write happen synchronously.
retry:
select {
case _, _ = <-d.closeCh:
return ErrClosed
case d.writeCh <- b:
if <-d.writeMergedCh {
return <-d.writeAckCh
case db.writeC <- b:
if <-db.writeMergedC {
return <-db.writeAckC
}
goto retry
case d.writeLockCh <- struct{}{}:
case db.writeLockC <- struct{}{}:
case _, _ = <-db.closeC:
return ErrClosed
}
merged := 0
defer func() {
<-d.writeLockCh
<-db.writeLockC
for i := 0; i < merged; i++ {
d.writeAckCh <- err
db.writeAckC <- err
}
}()
mem, memFree, err := d.flush(b.size())
mem, memFree, err := db.flush(b.size())
if err != nil {
return
}
defer mem.decref()
// Calculate maximum size of the batch.
m := 1 << 20
@@ -150,13 +164,13 @@ retry:
drain:
for b.size() < m && !b.sync {
select {
case nb := <-d.writeCh:
case nb := <-db.writeC:
if b.size()+nb.size() <= m {
b.append(nb)
d.writeMergedCh <- true
db.writeMergedC <- true
merged++
} else {
d.writeMergedCh <- false
db.writeMergedC <- false
break drain
}
default:
@@ -165,41 +179,45 @@ drain:
}
// Set batch first seq number relative from last seq.
b.seq = d.seq + 1
b.seq = db.seq + 1
// Write journal concurrently if it is large enough.
if b.size() >= (128 << 10) {
// Push the write batch to the journal writer
select {
case _, _ = <-d.closeCh:
case _, _ = <-db.closeC:
err = ErrClosed
return
case d.journalCh <- b:
case db.journalC <- b:
// Write into memdb
b.memReplay(mem)
b.memReplay(mem.mdb)
}
// Wait for journal writer
select {
case _, _ = <-d.closeCh:
case _, _ = <-db.closeC:
err = ErrClosed
return
case err = <-d.journalAckCh:
case err = <-db.journalAckC:
if err != nil {
// Revert memdb if error detected
b.revertMemReplay(mem)
b.revertMemReplay(mem.mdb)
return
}
}
} else {
err = d.doWriteJournal(b)
err = db.writeJournal(b)
if err != nil {
return
}
b.memReplay(mem)
b.memReplay(mem.mdb)
}
// Set last seq number.
d.addSeq(uint64(b.len()))
db.addSeq(uint64(b.len()))
if b.size() >= memFree {
db.rotateMem(0)
}
return
}
@@ -207,18 +225,66 @@ drain:
// for that key; a DB is not a multi-map.
//
// It is safe to modify the contents of the arguments after Put returns.
func (d *DB) Put(key, value []byte, wo *opt.WriteOptions) error {
func (db *DB) Put(key, value []byte, wo *opt.WriteOptions) error {
b := new(Batch)
b.Put(key, value)
return d.Write(b, wo)
return db.Write(b, wo)
}
// Delete deletes the value for the given key. It returns ErrNotFound if
// the DB does not contain the key.
//
// It is safe to modify the contents of the arguments after Delete returns.
func (d *DB) Delete(key []byte, wo *opt.WriteOptions) error {
func (db *DB) Delete(key []byte, wo *opt.WriteOptions) error {
b := new(Batch)
b.Delete(key)
return d.Write(b, wo)
return db.Write(b, wo)
}
func isMemOverlaps(icmp *iComparer, mem *memdb.DB, min, max []byte) bool {
iter := mem.NewIterator(nil)
defer iter.Release()
return (max == nil || (iter.First() && icmp.uCompare(max, iKey(iter.Key()).ukey()) >= 0)) &&
(min == nil || (iter.Last() && icmp.uCompare(min, iKey(iter.Key()).ukey()) <= 0))
}
// CompactRange compacts the underlying DB for the given key range.
// In particular, deleted and overwritten versions are discarded,
// and the data is rearranged to reduce the cost of operations
// needed to access the data. This operation should typically only
// be invoked by users who understand the underlying implementation.
//
// A nil Range.Start is treated as a key before all keys in the DB.
// And a nil Range.Limit is treated as a key after all keys in the DB.
// Therefore if both is nil then it will compact entire DB.
func (db *DB) CompactRange(r util.Range) error {
if err := db.ok(); err != nil {
return err
}
select {
case db.writeLockC <- struct{}{}:
case _, _ = <-db.closeC:
return ErrClosed
}
// Check for overlaps in memdb.
mem := db.getEffectiveMem()
defer mem.decref()
if isMemOverlaps(db.s.icmp, mem.mdb, r.Start, r.Limit) {
// Memdb compaction.
if _, err := db.rotateMem(0); err != nil {
<-db.writeLockC
return err
}
<-db.writeLockC
if err := db.compSendIdle(db.mcompCmdC); err != nil {
return err
}
} else {
<-db.writeLockC
}
// Table compaction.
return db.compSendRange(db.tcompCmdC, -1, r.Start, r.Limit)
}
@@ -37,6 +37,16 @@
// err = iter.Error()
// ...
//
// Iterate over subset of database content with a particular prefix:
// iter := db.NewIterator(util.BytesPrefix([]byte("foo-")), nil)
// for iter.Next() {
// // Use key/value.
// ...
// }
// iter.Release()
// err = iter.Error()
// ...
//
// Seek-then-Iterate:
//
// iter := db.NewIterator(nil, nil)
@@ -0,0 +1,38 @@
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package leveldb
import (
"errors"
"github.com/syndtr/goleveldb/leveldb/util"
)
var (
ErrNotFound = util.ErrNotFound
ErrSnapshotReleased = errors.New("leveldb: snapshot released")
ErrIterReleased = errors.New("leveldb: iterator released")
ErrClosed = errors.New("leveldb: closed")
)
type CorruptionType int
const (
CorruptedManifest CorruptionType = iota
MissingFiles
)
// ErrCorrupted is the type that wraps errors that indicate corruption in
// the database.
type ErrCorrupted struct {
Type CorruptionType
Err error
}
func (e ErrCorrupted) Error() string {
return e.Err.Error()
}
@@ -21,7 +21,7 @@ var _ = testutil.Defer(func() {
BlockRestartInterval: 5,
BlockSize: 50,
Compression: opt.NoCompression,
MaxOpenFiles: 0,
CachedOpenFiles: -1,
Strict: opt.StrictAll,
WriteBuffer: 1000,
}
@@ -36,22 +36,21 @@ var _ = testutil.Defer(func() {
testutil.DoDBTesting(&t)
db.TestClose()
done <- true
}, 9.0)
}, 20.0)
})
Describe("read test", func() {
testutil.AllKeyValueTesting(nil, func(kv testutil.KeyValue) testutil.DB {
testutil.AllKeyValueTesting(nil, nil, func(kv testutil.KeyValue) testutil.DB {
// Building the DB.
db := newTestingDB(o, nil, nil)
kv.IterateShuffled(nil, func(i int, key, value []byte) {
err := db.TestPut(key, value)
Expect(err).NotTo(HaveOccurred())
})
testutil.Defer("teardown", func() {
db.TestClose()
})
return db
}, func(db testutil.DB) {
db.(*testingDB).TestClose()
})
})
})
@@ -0,0 +1,58 @@
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// +build go1.3
package leveldb
import (
"sync/atomic"
"testing"
)
func BenchmarkDBReadConcurrent(b *testing.B) {
p := openDBBench(b, false)
p.populate(b.N)
p.fill()
p.gc()
defer p.close()
b.ResetTimer()
b.SetBytes(116)
b.RunParallel(func(pb *testing.PB) {
iter := p.newIter()
defer iter.Release()
for pb.Next() && iter.Next() {
}
})
}
func BenchmarkDBReadConcurrent2(b *testing.B) {
p := openDBBench(b, false)
p.populate(b.N)
p.fill()
p.gc()
defer p.close()
b.ResetTimer()
b.SetBytes(116)
var dir uint32
b.RunParallel(func(pb *testing.PB) {
iter := p.newIter()
defer iter.Release()
if atomic.AddUint32(&dir, 1)%2 == 0 {
for pb.Next() && iter.Next() {
}
} else {
if pb.Next() && iter.Last() {
for pb.Next() && iter.Prev() {
}
}
}
})
}
@@ -40,13 +40,19 @@ type basicArrayIterator struct {
util.BasicReleaser
array BasicArray
pos int
err error
}
func (i *basicArrayIterator) Valid() bool {
return i.pos >= 0 && i.pos < i.array.Len()
return i.pos >= 0 && i.pos < i.array.Len() && !i.Released()
}
func (i *basicArrayIterator) First() bool {
if i.Released() {
i.err = ErrIterReleased
return false
}
if i.array.Len() == 0 {
i.pos = -1
return false
@@ -56,6 +62,11 @@ func (i *basicArrayIterator) First() bool {
}
func (i *basicArrayIterator) Last() bool {
if i.Released() {
i.err = ErrIterReleased
return false
}
n := i.array.Len()
if n == 0 {
i.pos = 0
@@ -66,6 +77,11 @@ func (i *basicArrayIterator) Last() bool {
}
func (i *basicArrayIterator) Seek(key []byte) bool {
if i.Released() {
i.err = ErrIterReleased
return false
}
n := i.array.Len()
if n == 0 {
i.pos = 0
@@ -79,6 +95,11 @@ func (i *basicArrayIterator) Seek(key []byte) bool {
}
func (i *basicArrayIterator) Next() bool {
if i.Released() {
i.err = ErrIterReleased
return false
}
i.pos++
if n := i.array.Len(); i.pos >= n {
i.pos = n
@@ -88,6 +109,11 @@ func (i *basicArrayIterator) Next() bool {
}
func (i *basicArrayIterator) Prev() bool {
if i.Released() {
i.err = ErrIterReleased
return false
}
i.pos--
if i.pos < 0 {
i.pos = -1
@@ -96,7 +122,7 @@ func (i *basicArrayIterator) Prev() bool {
return true
}
func (i *basicArrayIterator) Error() error { return nil }
func (i *basicArrayIterator) Error() error { return i.err }
type arrayIterator struct {
basicArrayIterator
@@ -26,9 +26,10 @@ type indexedIterator struct {
strict bool
strictGet bool
data Iterator
err error
errf func(err error)
data Iterator
err error
errf func(err error)
closed bool
}
func (i *indexedIterator) setData() {
@@ -50,6 +51,15 @@ func (i *indexedIterator) clearData() {
i.data = nil
}
func (i *indexedIterator) indexErr() {
if err := i.index.Error(); err != nil {
if i.errf != nil {
i.errf(err)
}
i.err = err
}
}
func (i *indexedIterator) dataErr() bool {
if i.errf != nil {
if err := i.data.Error(); err != nil {
@@ -72,9 +82,13 @@ func (i *indexedIterator) Valid() bool {
func (i *indexedIterator) First() bool {
if i.err != nil {
return false
} else if i.Released() {
i.err = ErrIterReleased
return false
}
if !i.index.First() {
i.indexErr()
i.clearData()
return false
}
@@ -85,9 +99,13 @@ func (i *indexedIterator) First() bool {
func (i *indexedIterator) Last() bool {
if i.err != nil {
return false
} else if i.Released() {
i.err = ErrIterReleased
return false
}
if !i.index.Last() {
i.indexErr()
i.clearData()
return false
}
@@ -105,9 +123,13 @@ func (i *indexedIterator) Last() bool {
func (i *indexedIterator) Seek(key []byte) bool {
if i.err != nil {
return false
} else if i.Released() {
i.err = ErrIterReleased
return false
}
if !i.index.Seek(key) {
i.indexErr()
i.clearData()
return false
}
@@ -125,6 +147,9 @@ func (i *indexedIterator) Seek(key []byte) bool {
func (i *indexedIterator) Next() bool {
if i.err != nil {
return false
} else if i.Released() {
i.err = ErrIterReleased
return false
}
switch {
@@ -136,6 +161,7 @@ func (i *indexedIterator) Next() bool {
fallthrough
case i.data == nil:
if !i.index.Next() {
i.indexErr()
return false
}
i.setData()
@@ -147,6 +173,9 @@ func (i *indexedIterator) Next() bool {
func (i *indexedIterator) Prev() bool {
if i.err != nil {
return false
} else if i.Released() {
i.err = ErrIterReleased
return false
}
switch {
@@ -158,6 +187,7 @@ func (i *indexedIterator) Prev() bool {
fallthrough
case i.data == nil:
if !i.index.Prev() {
i.indexErr()
return false
}
i.setData()
@@ -14,6 +14,10 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
var (
ErrIterReleased = errors.New("leveldb/iterator: iterator released")
)
// IteratorSeeker is the interface that wraps the 'seeks method'.
type IteratorSeeker interface {
// First moves the iterator to the first key/value pair. If the iterator
@@ -100,28 +104,13 @@ type ErrorCallbackSetter interface {
}
type emptyIterator struct {
releaser util.Releaser
released bool
err error
util.BasicReleaser
err error
}
func (i *emptyIterator) rErr() {
if i.err == nil && i.released {
i.err = errors.New("leveldb/iterator: iterator released")
}
}
func (i *emptyIterator) Release() {
if i.releaser != nil {
i.releaser.Release()
i.releaser = nil
}
i.released = true
}
func (i *emptyIterator) SetReleaser(releaser util.Releaser) {
if !i.released {
i.releaser = releaser
if i.err == nil && i.Released() {
i.err = ErrIterReleased
}
}
@@ -7,16 +7,10 @@
package iterator
import (
"errors"
"github.com/syndtr/goleveldb/leveldb/comparer"
"github.com/syndtr/goleveldb/leveldb/util"
)
var (
ErrIterReleased = errors.New("leveldb/iterator: iterator released")
)
type dir int
const (
@@ -274,9 +268,13 @@ func (i *mergedIterator) Release() {
}
func (i *mergedIterator) SetReleaser(releaser util.Releaser) {
if i.dir != dirReleased {
i.releaser = releaser
if i.dir == dirReleased {
panic(util.ErrReleased)
}
if i.releaser != nil && releaser != nil {
panic(util.ErrHasReleaser)
}
i.releaser = releaser
}
func (i *mergedIterator) Error() error {
@@ -103,18 +103,18 @@ type flusher interface {
Flush() error
}
// DroppedError is the error type that passed to Dropper.Drop method.
type DroppedError struct {
// ErrCorrupted is the error type that generated by corrupted block or chunk.
type ErrCorrupted struct {
Size int
Reason string
}
func (e DroppedError) Error() string {
return fmt.Sprintf("leveldb/journal: dropped %d bytes: %s", e.Size, e.Reason)
func (e ErrCorrupted) Error() string {
return fmt.Sprintf("leveldb/journal: block/chunk corrupted: %s (%d bytes)", e.Reason, e.Size)
}
// Dropper is the interface that wrap simple Drop method. The Drop
// method will be called when the journal reader dropping a chunk.
// method will be called when the journal reader dropping a block or chunk.
type Dropper interface {
Drop(err error)
}
@@ -158,76 +158,78 @@ func NewReader(r io.Reader, dropper Dropper, strict, checksum bool) *Reader {
}
}
var errSkip = errors.New("leveldb/journal: skipped")
func (r *Reader) corrupt(n int, reason string, skip bool) error {
if r.dropper != nil {
r.dropper.Drop(ErrCorrupted{n, reason})
}
if r.strict && !skip {
r.err = ErrCorrupted{n, reason}
return r.err
}
return errSkip
}
// nextChunk sets r.buf[r.i:r.j] to hold the next chunk's payload, reading the
// next block into the buffer if necessary.
func (r *Reader) nextChunk(wantFirst, skip bool) error {
func (r *Reader) nextChunk(first bool) error {
for {
if r.j+headerSize <= r.n {
checksum := binary.LittleEndian.Uint32(r.buf[r.j+0 : r.j+4])
length := binary.LittleEndian.Uint16(r.buf[r.j+4 : r.j+6])
chunkType := r.buf[r.j+6]
var err error
if checksum == 0 && length == 0 && chunkType == 0 {
// Drop entire block.
err = DroppedError{r.n - r.j, "zero header"}
m := r.n - r.j
r.i = r.n
r.j = r.n
return r.corrupt(m, "zero header", false)
} else {
m := r.n - r.j
r.i = r.j + headerSize
r.j = r.j + headerSize + int(length)
if r.j > r.n {
// Drop entire block.
err = DroppedError{m, "chunk length overflows block"}
r.i = r.n
r.j = r.n
return r.corrupt(m, "chunk length overflows block", false)
} else if r.checksum && checksum != util.NewCRC(r.buf[r.i-1:r.j]).Value() {
// Drop entire block.
err = DroppedError{m, "checksum mismatch"}
r.i = r.n
r.j = r.n
return r.corrupt(m, "checksum mismatch", false)
}
}
if wantFirst && err == nil && chunkType != fullChunkType && chunkType != firstChunkType {
if skip {
// The chunk are intentionally skipped.
if chunkType == lastChunkType {
skip = false
}
continue
} else {
// Drop the chunk.
err = DroppedError{r.j - r.i + headerSize, "orphan chunk"}
}
if first && chunkType != fullChunkType && chunkType != firstChunkType {
m := r.j - r.i
r.i = r.j
// Report the error, but skip it.
return r.corrupt(m+headerSize, "orphan chunk", true)
}
if err == nil {
r.last = chunkType == fullChunkType || chunkType == lastChunkType
} else {
if r.dropper != nil {
r.dropper.Drop(err)
}
if r.strict {
r.err = err
}
r.last = chunkType == fullChunkType || chunkType == lastChunkType
return nil
}
// The last block.
if r.n < blockSize && r.n > 0 {
if !first {
return r.corrupt(0, "missing chunk part", false)
}
r.err = io.EOF
return r.err
}
// Read block.
n, err := io.ReadFull(r.r, r.buf[:])
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return err
}
if r.n < blockSize && r.n > 0 {
// This is the last block.
if r.j != r.n {
r.err = io.ErrUnexpectedEOF
} else {
r.err = io.EOF
}
return r.err
}
n, err := io.ReadFull(r.r, r.buf[:])
if err != nil && err != io.ErrUnexpectedEOF {
r.err = err
return r.err
}
if n == 0 {
if !first {
return r.corrupt(0, "missing chunk part", false)
}
r.err = io.EOF
return r.err
}
@@ -237,29 +239,26 @@ func (r *Reader) nextChunk(wantFirst, skip bool) error {
// Next returns a reader for the next journal. It returns io.EOF if there are no
// more journals. The reader returned becomes stale after the next Next call,
// and should no longer be used.
// and should no longer be used. If strict is false, the reader will returns
// io.ErrUnexpectedEOF error when found corrupted journal.
func (r *Reader) Next() (io.Reader, error) {
r.seq++
if r.err != nil {
return nil, r.err
}
skip := !r.last
r.i = r.j
for {
r.i = r.j
if r.nextChunk(true, skip) != nil {
// So that 'orphan chunk' drop will be reported.
skip = false
} else {
if err := r.nextChunk(true); err == nil {
break
}
if r.err != nil {
return nil, r.err
} else if err != errSkip {
return nil, err
}
}
return &singleReader{r, r.seq, nil}, nil
}
// Reset resets the journal reader, allows reuse of the journal reader.
// Reset resets the journal reader, allows reuse of the journal reader. Reset returns
// last accumulated error.
func (r *Reader) Reset(reader io.Reader, dropper Dropper, strict, checksum bool) error {
r.seq++
err := r.err
@@ -296,7 +295,11 @@ func (x *singleReader) Read(p []byte) (int, error) {
if r.last {
return 0, io.EOF
}
if x.err = r.nextChunk(false, false); x.err != nil {
x.err = r.nextChunk(false)
if x.err != nil {
if x.err == errSkip {
x.err = io.ErrUnexpectedEOF
}
return 0, x.err
}
}
@@ -320,7 +323,11 @@ func (x *singleReader) ReadByte() (byte, error) {
if r.last {
return 0, io.EOF
}
if x.err = r.nextChunk(false, false); x.err != nil {
x.err = r.nextChunk(false)
if x.err != nil {
if x.err == errSkip {
x.err = io.ErrUnexpectedEOF
}
return 0, x.err
}
}
@@ -12,6 +12,7 @@ package journal
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"io/ioutil"
@@ -326,3 +327,492 @@ func TestStaleWriter(t *testing.T) {
t.Fatalf("stale write #1: unexpected error: %v", err)
}
}
func TestCorrupt_MissingLastBlock(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
// First record.
ww, err := w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-1024)); err != nil {
t.Fatalf("write #0: unexpected error: %v", err)
}
// Second record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
t.Fatalf("write #1: unexpected error: %v", err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
// Cut the last block.
b := buf.Bytes()[:blockSize]
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
// First read.
rr, err := r.Next()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #0: %v", err)
}
if n != blockSize-1024 {
t.Fatalf("read #0: got %d bytes want %d", n, blockSize-1024)
}
// Second read.
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != io.ErrUnexpectedEOF {
t.Fatalf("read #1: unexpected error: %v", err)
}
if _, err := r.Next(); err != io.EOF {
t.Fatalf("last next: unexpected error: %v", err)
}
}
func TestCorrupt_CorruptedFirstBlock(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
// First record.
ww, err := w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize/2)); err != nil {
t.Fatalf("write #0: unexpected error: %v", err)
}
// Second record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
t.Fatalf("write #1: unexpected error: %v", err)
}
// Third record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+1)); err != nil {
t.Fatalf("write #2: unexpected error: %v", err)
}
// Fourth record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+2)); err != nil {
t.Fatalf("write #3: unexpected error: %v", err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
b := buf.Bytes()
// Corrupting block #0.
for i := 0; i < 1024; i++ {
b[i] = '1'
}
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
// First read (third record).
rr, err := r.Next()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #0: %v", err)
}
if want := int64(blockSize-headerSize) + 1; n != want {
t.Fatalf("read #0: got %d bytes want %d", n, want)
}
// Second read (fourth record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #1: %v", err)
}
if want := int64(blockSize-headerSize) + 2; n != want {
t.Fatalf("read #1: got %d bytes want %d", n, want)
}
if _, err := r.Next(); err != io.EOF {
t.Fatalf("last next: unexpected error: %v", err)
}
}
func TestCorrupt_CorruptedMiddleBlock(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
// First record.
ww, err := w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize/2)); err != nil {
t.Fatalf("write #0: unexpected error: %v", err)
}
// Second record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
t.Fatalf("write #1: unexpected error: %v", err)
}
// Third record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+1)); err != nil {
t.Fatalf("write #2: unexpected error: %v", err)
}
// Fourth record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+2)); err != nil {
t.Fatalf("write #3: unexpected error: %v", err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
b := buf.Bytes()
// Corrupting block #1.
for i := 0; i < 1024; i++ {
b[blockSize+i] = '1'
}
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
// First read (first record).
rr, err := r.Next()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #0: %v", err)
}
if want := int64(blockSize / 2); n != want {
t.Fatalf("read #0: got %d bytes want %d", n, want)
}
// Second read (second record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != io.ErrUnexpectedEOF {
t.Fatalf("read #1: unexpected error: %v", err)
}
// Third read (fourth record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #2: %v", err)
}
if want := int64(blockSize-headerSize) + 2; n != want {
t.Fatalf("read #2: got %d bytes want %d", n, want)
}
if _, err := r.Next(); err != io.EOF {
t.Fatalf("last next: unexpected error: %v", err)
}
}
func TestCorrupt_CorruptedLastBlock(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
// First record.
ww, err := w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize/2)); err != nil {
t.Fatalf("write #0: unexpected error: %v", err)
}
// Second record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
t.Fatalf("write #1: unexpected error: %v", err)
}
// Third record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+1)); err != nil {
t.Fatalf("write #2: unexpected error: %v", err)
}
// Fourth record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+2)); err != nil {
t.Fatalf("write #3: unexpected error: %v", err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
b := buf.Bytes()
// Corrupting block #3.
for i := len(b) - 1; i > len(b)-1024; i-- {
b[i] = '1'
}
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
// First read (first record).
rr, err := r.Next()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #0: %v", err)
}
if want := int64(blockSize / 2); n != want {
t.Fatalf("read #0: got %d bytes want %d", n, want)
}
// Second read (second record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #1: %v", err)
}
if want := int64(blockSize - headerSize); n != want {
t.Fatalf("read #1: got %d bytes want %d", n, want)
}
// Third read (third record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #2: %v", err)
}
if want := int64(blockSize-headerSize) + 1; n != want {
t.Fatalf("read #2: got %d bytes want %d", n, want)
}
// Fourth read (fourth record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != io.ErrUnexpectedEOF {
t.Fatalf("read #3: unexpected error: %v", err)
}
if _, err := r.Next(); err != io.EOF {
t.Fatalf("last next: unexpected error: %v", err)
}
}
func TestCorrupt_FirstChuckLengthOverflow(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
// First record.
ww, err := w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize/2)); err != nil {
t.Fatalf("write #0: unexpected error: %v", err)
}
// Second record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
t.Fatalf("write #1: unexpected error: %v", err)
}
// Third record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+1)); err != nil {
t.Fatalf("write #2: unexpected error: %v", err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
b := buf.Bytes()
// Corrupting record #1.
x := blockSize
binary.LittleEndian.PutUint16(b[x+4:], 0xffff)
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
// First read (first record).
rr, err := r.Next()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #0: %v", err)
}
if want := int64(blockSize / 2); n != want {
t.Fatalf("read #0: got %d bytes want %d", n, want)
}
// Second read (second record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != io.ErrUnexpectedEOF {
t.Fatalf("read #1: unexpected error: %v", err)
}
if _, err := r.Next(); err != io.EOF {
t.Fatalf("last next: unexpected error: %v", err)
}
}
func TestCorrupt_MiddleChuckLengthOverflow(t *testing.T) {
buf := new(bytes.Buffer)
w := NewWriter(buf)
// First record.
ww, err := w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize/2)); err != nil {
t.Fatalf("write #0: unexpected error: %v", err)
}
// Second record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), blockSize-headerSize)); err != nil {
t.Fatalf("write #1: unexpected error: %v", err)
}
// Third record.
ww, err = w.Next()
if err != nil {
t.Fatal(err)
}
if _, err := ww.Write(bytes.Repeat([]byte("0"), (blockSize-headerSize)+1)); err != nil {
t.Fatalf("write #2: unexpected error: %v", err)
}
if err := w.Close(); err != nil {
t.Fatal(err)
}
b := buf.Bytes()
// Corrupting record #1.
x := blockSize/2 + headerSize
binary.LittleEndian.PutUint16(b[x+4:], 0xffff)
r := NewReader(bytes.NewReader(b), dropper{t}, false, true)
// First read (first record).
rr, err := r.Next()
if err != nil {
t.Fatal(err)
}
n, err := io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #0: %v", err)
}
if want := int64(blockSize / 2); n != want {
t.Fatalf("read #0: got %d bytes want %d", n, want)
}
// Second read (third record).
rr, err = r.Next()
if err != nil {
t.Fatal(err)
}
n, err = io.Copy(ioutil.Discard, rr)
if err != nil {
t.Fatalf("read #1: %v", err)
}
if want := int64(blockSize-headerSize) + 1; n != want {
t.Fatalf("read #1: got %d bytes want %d", n, want)
}
if _, err := r.Next(); err != io.EOF {
t.Fatalf("last next: unexpected error: %v", err)
}
}
@@ -13,7 +13,7 @@ import (
"github.com/syndtr/goleveldb/leveldb/comparer"
)
var icmp = &iComparer{comparer.DefaultComparer}
var defaultIComparer = &iComparer{comparer.DefaultComparer}
func ikey(key string, seq uint64, t vType) iKey {
return newIKey([]byte(key), uint64(seq), t)
@@ -21,7 +21,7 @@ func ikey(key string, seq uint64, t vType) iKey {
func shortSep(a, b []byte) []byte {
dst := make([]byte, len(a))
dst = icmp.Separator(dst[:0], a, b)
dst = defaultIComparer.Separator(dst[:0], a, b)
if dst == nil {
return a
}
@@ -30,7 +30,7 @@ func shortSep(a, b []byte) []byte {
func shortSuccessor(b []byte) []byte {
dst := make([]byte, len(b))
dst = icmp.Successor(dst[:0], b)
dst = defaultIComparer.Successor(dst[:0], b)
if dst == nil {
return b
}
@@ -8,6 +8,7 @@
package memdb
import (
"errors"
"math/rand"
"sync"
@@ -17,7 +18,8 @@ import (
)
var (
ErrNotFound = util.ErrNotFound
ErrNotFound = util.ErrNotFound
ErrIterReleased = errors.New("leveldb/memdb: iterator released")
)
const tMaxHeight = 12
@@ -29,6 +31,7 @@ type dbIter struct {
node int
forward bool
key, value []byte
err error
}
func (i *dbIter) fill(checkStart, checkLimit bool) bool {
@@ -59,6 +62,11 @@ func (i *dbIter) Valid() bool {
}
func (i *dbIter) First() bool {
if i.Released() {
i.err = ErrIterReleased
return false
}
i.forward = true
i.p.mu.RLock()
defer i.p.mu.RUnlock()
@@ -71,9 +79,11 @@ func (i *dbIter) First() bool {
}
func (i *dbIter) Last() bool {
if i.p == nil {
if i.Released() {
i.err = ErrIterReleased
return false
}
i.forward = false
i.p.mu.RLock()
defer i.p.mu.RUnlock()
@@ -86,9 +96,11 @@ func (i *dbIter) Last() bool {
}
func (i *dbIter) Seek(key []byte) bool {
if i.p == nil {
if i.Released() {
i.err = ErrIterReleased
return false
}
i.forward = true
i.p.mu.RLock()
defer i.p.mu.RUnlock()
@@ -100,9 +112,11 @@ func (i *dbIter) Seek(key []byte) bool {
}
func (i *dbIter) Next() bool {
if i.p == nil {
if i.Released() {
i.err = ErrIterReleased
return false
}
if i.node == 0 {
if !i.forward {
return i.First()
@@ -117,9 +131,11 @@ func (i *dbIter) Next() bool {
}
func (i *dbIter) Prev() bool {
if i.p == nil {
if i.Released() {
i.err = ErrIterReleased
return false
}
if i.node == 0 {
if i.forward {
return i.Last()
@@ -141,10 +157,10 @@ func (i *dbIter) Value() []byte {
return i.value
}
func (i *dbIter) Error() error { return nil }
func (i *dbIter) Error() error { return i.err }
func (i *dbIter) Release() {
if i.p != nil {
if !i.Released() {
i.p = nil
i.node = 0
i.key = nil
@@ -437,6 +453,8 @@ func (p *DB) Reset() {
// New creates a new initalized in-memory key/value DB. The capacity
// is the initial key/value buffer capacity. The capacity is advisory,
// not enforced.
//
// The returned DB instance is goroutine-safe.
func New(cmp comparer.BasicComparer, capacity int) *DB {
p := &DB{
cmp: cmp,
@@ -129,7 +129,7 @@ var _ = testutil.Defer(func() {
}
return db
})
}, nil, nil)
})
})
})
@@ -24,16 +24,22 @@ const (
DefaultBlockRestartInterval = 16
DefaultBlockSize = 4 * KiB
DefaultCompressionType = SnappyCompression
DefaultMaxOpenFiles = 1000
DefaultCachedOpenFiles = 500
DefaultWriteBuffer = 4 * MiB
)
type noCache struct{}
func (noCache) SetCapacity(capacity int) {}
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
func (noCache) Purge(fin cache.PurgeFin) {}
func (noCache) Zap(closed bool) {}
func (noCache) SetCapacity(capacity int) {}
func (noCache) Capacity() int { return 0 }
func (noCache) Used() int { return 0 }
func (noCache) Size() int { return 0 }
func (noCache) NumObjects() int { return 0 }
func (noCache) GetNamespace(id uint64) cache.Namespace { return nil }
func (noCache) PurgeNamespace(id uint64, fin cache.PurgeFin) {}
func (noCache) ZapNamespace(id uint64) {}
func (noCache) Purge(fin cache.PurgeFin) {}
func (noCache) Zap() {}
var NoCache cache.Cache = noCache{}
@@ -119,6 +125,13 @@ type Options struct {
// The default value is 4KiB.
BlockSize int
// CachedOpenFiles defines number of open files to kept around when not
// in-use, the counting includes still in-use files.
// Set this to negative value to disable caching.
//
// The default value is 500.
CachedOpenFiles int
// Comparer defines a total ordering over the space of []byte keys: a 'less
// than' relationship. The same comparison algorithm must be used for reads
// and writes over the lifetime of the DB.
@@ -159,13 +172,6 @@ type Options struct {
// The default value is nil.
Filter filter.Filter
// MaxOpenFiles defines maximum number of open files to kept around
// (cached). This is not an hard limit, actual open files may exceed
// the defined value.
//
// The default value is 1000.
MaxOpenFiles int
// Strict defines the DB strict level.
Strict Strict
@@ -207,6 +213,15 @@ func (o *Options) GetBlockSize() int {
return o.BlockSize
}
func (o *Options) GetCachedOpenFiles() int {
if o == nil || o.CachedOpenFiles == 0 {
return DefaultCachedOpenFiles
} else if o.CachedOpenFiles < 0 {
return 0
}
return o.CachedOpenFiles
}
func (o *Options) GetComparer() comparer.Comparer {
if o == nil || o.Comparer == nil {
return comparer.DefaultComparer
@@ -242,13 +257,6 @@ func (o *Options) GetFilter() filter.Filter {
return o.Filter
}
func (o *Options) GetMaxOpenFiles() int {
if o == nil || o.MaxOpenFiles <= 0 {
return DefaultMaxOpenFiles
}
return o.MaxOpenFiles
}
func (o *Options) GetStrict(strict Strict) bool {
if o == nil || o.Strict == 0 {
return DefaultStrict&strict != 0
@@ -32,8 +32,8 @@ func (s *session) setOptions(o *opt.Options) {
s.o.BlockCache = nil
}
// Comparer.
s.cmp = &iComparer{o.GetComparer()}
s.o.Comparer = s.cmp
s.icmp = &iComparer{o.GetComparer()}
s.o.Comparer = s.icmp
// Filter.
if filter := o.GetFilter(); filter != nil {
s.o.Filter = &iFilter{filter}
@@ -20,16 +20,6 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
// ErrManifest is the type that wraps errors produced by missing
// or corrupted manifest file.
type ErrManifest struct {
Err error
}
func (e ErrManifest) Error() string {
return e.Err.Error()
}
// session represent a persistent database session.
type session struct {
// Need 64-bit alignment.
@@ -42,18 +32,19 @@ type session struct {
stor storage.Storage
storLock util.Releaser
o *opt.Options
cmp *iComparer
icmp *iComparer
tops *tOps
manifest *journal.Writer
manifestWriter storage.Writer
manifestFile storage.File
stCPtrs [kNumLevels]iKey // compact pointers; need external synchronization
stCptrs [kNumLevels]iKey // compact pointers; need external synchronization
stVersion *version // current version
vmu sync.Mutex
}
// Creates new initialized session instance.
func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) {
if stor == nil {
return nil, os.ErrInvalid
@@ -67,7 +58,7 @@ func newSession(stor storage.Storage, o *opt.Options) (s *session, err error) {
storLock: storLock,
}
s.setOptions(o)
s.tops = newTableOps(s, s.o.GetMaxOpenFiles())
s.tops = newTableOps(s, s.o.GetCachedOpenFiles())
s.setVersion(&version{s: s})
s.log("log@legend F·NumFile S·FileSize N·Entry C·BadEntry B·BadBlock D·DeletedEntry L·Level Q·SeqNum T·TimeElapsed")
return
@@ -91,6 +82,7 @@ func (s *session) close() {
s.stVersion = nil
}
// Release session lock.
func (s *session) release() {
s.storLock.Release()
}
@@ -108,7 +100,7 @@ func (s *session) recover() (err error) {
// Don't return os.ErrNotExist if the underlying storage contains
// other files that belong to LevelDB. So the DB won't get trashed.
if files, _ := s.stor.GetFiles(storage.TypeAll); len(files) > 0 {
err = ErrManifest{Err: errors.New("leveldb: manifest file missing")}
err = ErrCorrupted{Type: CorruptedManifest, Err: errors.New("leveldb: manifest file missing")}
}
}
}()
@@ -142,13 +134,13 @@ func (s *session) recover() (err error) {
err = rec.decode(r)
if err == nil {
// save compact pointers
for _, rp := range rec.compactionPointers {
s.stCPtrs[rp.level] = iKey(rp.key)
for _, r := range rec.compactionPointers {
s.stCptrs[r.level] = iKey(r.ikey)
}
// commit record to version staging
staging.commit(rec)
} else if strict {
return ErrManifest{Err: err}
return ErrCorrupted{Type: CorruptedManifest, Err: err}
} else {
s.logf("manifest error: %v (skipped)", err)
}
@@ -159,15 +151,15 @@ func (s *session) recover() (err error) {
switch {
case !rec.has(recComparer):
return ErrManifest{Err: errors.New("leveldb: manifest missing comparer name")}
case rec.comparer != s.cmp.cmp.Name():
return ErrManifest{Err: errors.New("leveldb: comparer mismatch, " + "want '" + s.cmp.cmp.Name() + "', " + "got '" + rec.comparer + "'")}
return ErrCorrupted{Type: CorruptedManifest, Err: errors.New("leveldb: manifest missing comparer name")}
case rec.comparer != s.icmp.uName():
return ErrCorrupted{Type: CorruptedManifest, Err: errors.New("leveldb: comparer mismatch, " + "want '" + s.icmp.uName() + "', " + "got '" + rec.comparer + "'")}
case !rec.has(recNextNum):
return ErrManifest{Err: errors.New("leveldb: manifest missing next file number")}
return ErrCorrupted{Type: CorruptedManifest, Err: errors.New("leveldb: manifest missing next file number")}
case !rec.has(recJournalNum):
return ErrManifest{Err: errors.New("leveldb: manifest missing journal file number")}
return ErrCorrupted{Type: CorruptedManifest, Err: errors.New("leveldb: manifest missing journal file number")}
case !rec.has(recSeq):
return ErrManifest{Err: errors.New("leveldb: manifest missing seq number")}
return ErrCorrupted{Type: CorruptedManifest, Err: errors.New("leveldb: manifest missing seq number")}
}
s.manifestFile = file
@@ -199,25 +191,22 @@ func (s *session) commit(r *sessionRecord) (err error) {
// Pick a compaction based on current state; need external synchronization.
func (s *session) pickCompaction() *compaction {
icmp := s.cmp
ucmp := icmp.cmp
v := s.version_NB()
var level int
var t0 tFiles
if v.cScore >= 1 {
level = v.cLevel
cp := s.stCPtrs[level]
tt := v.tables[level]
for _, t := range tt {
if cp == nil || icmp.Compare(t.max, cp) > 0 {
cptr := s.stCptrs[level]
tables := v.tables[level]
for _, t := range tables {
if cptr == nil || s.icmp.Compare(t.imax, cptr) > 0 {
t0 = append(t0, t)
break
}
}
if len(t0) == 0 {
t0 = append(t0, tt[0])
t0 = append(t0, tables[0])
}
} else {
if p := atomic.LoadPointer(&v.cSeek); p != nil {
@@ -229,11 +218,10 @@ func (s *session) pickCompaction() *compaction {
}
}
c := &compaction{s: s, version: v, level: level}
c := &compaction{s: s, v: v, level: level}
if level == 0 {
min, max := t0.getRange(icmp)
t0 = nil
v.tables[0].getOverlaps(min.ukey(), max.ukey(), &t0, false, ucmp)
imin, imax := t0.getRange(s.icmp)
t0 = v.tables[0].getOverlaps(t0[:0], s.icmp, imin.ukey(), imax.ukey(), true)
}
c.tables[0] = t0
@@ -242,25 +230,41 @@ func (s *session) pickCompaction() *compaction {
}
// Create compaction from given level and range; need external synchronization.
func (s *session) getCompactionRange(level int, min, max []byte) *compaction {
func (s *session) getCompactionRange(level int, umin, umax []byte) *compaction {
v := s.version_NB()
var t0 tFiles
v.tables[level].getOverlaps(min, max, &t0, level != 0, s.cmp.cmp)
t0 := v.tables[level].getOverlaps(nil, s.icmp, umin, umax, level == 0)
if len(t0) == 0 {
return nil
}
c := &compaction{s: s, version: v, level: level}
// Avoid compacting too much in one shot in case the range is large.
// But we cannot do this for level-0 since level-0 files can overlap
// and we must not pick one file and drop another older file if the
// two files overlap.
if level > 0 {
limit := uint64(kMaxTableSize)
total := uint64(0)
for i, t := range t0 {
total += t.size
if total >= limit {
s.logf("table@compaction limiting F·%d -> F·%d", len(t0), i+1)
t0 = t0[:i+1]
break
}
}
}
c := &compaction{s: s, v: v, level: level}
c.tables[0] = t0
c.expand()
return c
}
// compaction represent a compaction state
// compaction represent a compaction state.
type compaction struct {
s *session
version *version
s *session
v *version
level int
tables [2]tFiles
@@ -269,44 +273,36 @@ type compaction struct {
gpidx int
seenKey bool
overlappedBytes uint64
min, max iKey
imin, imax iKey
tPtrs [kNumLevels]int
}
// Expand compacted tables; need external synchronization.
func (c *compaction) expand() {
s := c.s
v := c.version
icmp := s.cmp
ucmp := icmp.cmp
level := c.level
vt0, vt1 := v.tables[level], v.tables[level+1]
vt0, vt1 := c.v.tables[level], c.v.tables[level+1]
t0, t1 := c.tables[0], c.tables[1]
min, max := t0.getRange(icmp)
vt1.getOverlaps(min.ukey(), max.ukey(), &t1, true, ucmp)
// Get entire range covered by compaction
amin, amax := append(t0, t1...).getRange(icmp)
imin, imax := t0.getRange(c.s.icmp)
t1 = vt1.getOverlaps(t1, c.s.icmp, imin.ukey(), imax.ukey(), false)
// Get entire range covered by compaction.
amin, amax := append(t0, t1...).getRange(c.s.icmp)
// See if we can grow the number of inputs in "level" without
// changing the number of "level+1" files we pick up.
if len(t1) > 0 {
var exp0 tFiles
vt0.getOverlaps(amin.ukey(), amax.ukey(), &exp0, level != 0, ucmp)
exp0 := vt0.getOverlaps(nil, c.s.icmp, amin.ukey(), amax.ukey(), level == 0)
if len(exp0) > len(t0) && t1.size()+exp0.size() < kExpCompactionMaxBytes {
var exp1 tFiles
xmin, xmax := exp0.getRange(icmp)
vt1.getOverlaps(xmin.ukey(), xmax.ukey(), &exp1, true, ucmp)
xmin, xmax := exp0.getRange(c.s.icmp)
exp1 := vt1.getOverlaps(nil, c.s.icmp, xmin.ukey(), xmax.ukey(), false)
if len(exp1) == len(t1) {
s.logf("table@compaction expanding L%d+L%d (F·%d S·%s)+(F·%d S·%s) -> (F·%d S·%s)+(F·%d S·%s)",
c.s.logf("table@compaction expanding L%d+L%d (F·%d S·%s)+(F·%d S·%s) -> (F·%d S·%s)+(F·%d S·%s)",
level, level+1, len(t0), shortenb(int(t0.size())), len(t1), shortenb(int(t1.size())),
len(exp0), shortenb(int(exp0.size())), len(exp1), shortenb(int(exp1.size())))
min, max = xmin, xmax
imin, imax = xmin, xmax
t0, t1 = exp0, exp1
amin, amax = append(t0, t1...).getRange(icmp)
amin, amax = append(t0, t1...).getRange(c.s.icmp)
}
}
}
@@ -314,11 +310,11 @@ func (c *compaction) expand() {
// Compute the set of grandparent files that overlap this compaction
// (parent == level+1; grandparent == level+2)
if level+2 < kNumLevels {
v.tables[level+2].getOverlaps(amin.ukey(), amax.ukey(), &c.gp, true, ucmp)
c.gp = c.v.tables[level+2].getOverlaps(c.gp, c.s.icmp, amin.ukey(), amax.ukey(), false)
}
c.tables[0], c.tables[1] = t0, t1
c.min, c.max = min, max
c.imin, c.imax = imin, imax
}
// Check whether compaction is trivial.
@@ -326,17 +322,14 @@ func (c *compaction) trivial() bool {
return len(c.tables[0]) == 1 && len(c.tables[1]) == 0 && c.gp.size() <= kMaxGrandParentOverlapBytes
}
func (c *compaction) isBaseLevelForKey(key []byte) bool {
s := c.s
v := c.version
ucmp := s.cmp.cmp
for level, tt := range v.tables[c.level+2:] {
for c.tPtrs[level] < len(tt) {
t := tt[c.tPtrs[level]]
if ucmp.Compare(key, t.max.ukey()) <= 0 {
// We've advanced far enough
if ucmp.Compare(key, t.min.ukey()) >= 0 {
// Key falls in this file's range, so definitely not base level
func (c *compaction) baseLevelForKey(ukey []byte) bool {
for level, tables := range c.v.tables[c.level+2:] {
for c.tPtrs[level] < len(tables) {
t := tables[c.tPtrs[level]]
if c.s.icmp.uCompare(ukey, t.imax.ukey()) <= 0 {
// We've advanced far enough.
if c.s.icmp.uCompare(ukey, t.imin.ukey()) >= 0 {
// Key falls in this file's range, so definitely not base level.
return false
}
break
@@ -347,11 +340,10 @@ func (c *compaction) isBaseLevelForKey(key []byte) bool {
return true
}
func (c *compaction) shouldStopBefore(key iKey) bool {
icmp := c.s.cmp
func (c *compaction) shouldStopBefore(ikey iKey) bool {
for ; c.gpidx < len(c.gp); c.gpidx++ {
gp := c.gp[c.gpidx]
if icmp.Compare(key, gp.max) <= 0 {
if c.s.icmp.Compare(ikey, gp.imax) <= 0 {
break
}
if c.seenKey {
@@ -361,43 +353,44 @@ func (c *compaction) shouldStopBefore(key iKey) bool {
c.seenKey = true
if c.overlappedBytes > kMaxGrandParentOverlapBytes {
// Too much overlap for current output; start new output
// Too much overlap for current output; start new output.
c.overlappedBytes = 0
return true
}
return false
}
// Creates an iterator.
func (c *compaction) newIterator() iterator.Iterator {
s := c.s
icmp := s.cmp
level := c.level
icap := 2
// Creates iterator slice.
icap := len(c.tables)
if c.level == 0 {
// Special case for level-0
icap = len(c.tables[0]) + 1
}
its := make([]iterator.Iterator, 0, icap)
// Options.
ro := &opt.ReadOptions{
DontFillCache: true,
}
strict := s.o.GetStrict(opt.StrictIterator)
strict := c.s.o.GetStrict(opt.StrictIterator)
for i, tt := range c.tables {
if len(tt) == 0 {
for i, tables := range c.tables {
if len(tables) == 0 {
continue
}
if level+i == 0 {
for _, t := range tt {
its = append(its, s.tops.newIterator(t, nil, ro))
// Level-0 is not sorted and may overlaps each other.
if c.level+i == 0 {
for _, t := range tables {
its = append(its, c.s.tops.newIterator(t, nil, ro))
}
} else {
it := iterator.NewIndexedIterator(tt.newIndexIterator(s.tops, icmp, nil, ro), strict, true)
it := iterator.NewIndexedIterator(tables.newIndexIterator(c.s.tops, c.s.icmp, nil, ro), strict, true)
its = append(its, it)
}
}
return iterator.NewMergedIterator(its, icmp, true)
return iterator.NewMergedIterator(its, c.s.icmp, true)
}
@@ -35,19 +35,19 @@ const (
type cpRecord struct {
level int
key iKey
ikey iKey
}
type ntRecord struct {
level int
num uint64
size uint64
min iKey
max iKey
imin iKey
imax iKey
}
func (r ntRecord) makeFile(s *session) *tFile {
return newTFile(s.getTableFile(r.num), r.size, r.min, r.max)
return newTableFile(s.getTableFile(r.num), r.size, r.imin, r.imax)
}
type dtRecord struct {
@@ -98,9 +98,9 @@ func (p *sessionRecord) setSeq(seq uint64) {
p.seq = seq
}
func (p *sessionRecord) addCompactionPointer(level int, key iKey) {
func (p *sessionRecord) addCompactionPointer(level int, ikey iKey) {
p.hasRec |= 1 << recCompactionPointer
p.compactionPointers = append(p.compactionPointers, cpRecord{level, key})
p.compactionPointers = append(p.compactionPointers, cpRecord{level, ikey})
}
func (p *sessionRecord) resetCompactionPointers() {
@@ -108,13 +108,13 @@ func (p *sessionRecord) resetCompactionPointers() {
p.compactionPointers = p.compactionPointers[:0]
}
func (p *sessionRecord) addTable(level int, num, size uint64, min, max iKey) {
func (p *sessionRecord) addTable(level int, num, size uint64, imin, imax iKey) {
p.hasRec |= 1 << recNewTable
p.addedTables = append(p.addedTables, ntRecord{level, num, size, min, max})
p.addedTables = append(p.addedTables, ntRecord{level, num, size, imin, imax})
}
func (p *sessionRecord) addTableFile(level int, t *tFile) {
p.addTable(level, t.file.Num(), t.size, t.min, t.max)
p.addTable(level, t.file.Num(), t.size, t.imin, t.imax)
}
func (p *sessionRecord) resetAddedTables() {
@@ -169,23 +169,23 @@ func (p *sessionRecord) encode(w io.Writer) error {
p.putUvarint(w, recSeq)
p.putUvarint(w, p.seq)
}
for _, cp := range p.compactionPointers {
for _, r := range p.compactionPointers {
p.putUvarint(w, recCompactionPointer)
p.putUvarint(w, uint64(cp.level))
p.putBytes(w, cp.key)
p.putUvarint(w, uint64(r.level))
p.putBytes(w, r.ikey)
}
for _, t := range p.deletedTables {
for _, r := range p.deletedTables {
p.putUvarint(w, recDeletedTable)
p.putUvarint(w, uint64(t.level))
p.putUvarint(w, t.num)
p.putUvarint(w, uint64(r.level))
p.putUvarint(w, r.num)
}
for _, t := range p.addedTables {
for _, r := range p.addedTables {
p.putUvarint(w, recNewTable)
p.putUvarint(w, uint64(t.level))
p.putUvarint(w, t.num)
p.putUvarint(w, t.size)
p.putBytes(w, t.min)
p.putBytes(w, t.max)
p.putUvarint(w, uint64(r.level))
p.putUvarint(w, r.num)
p.putUvarint(w, r.size)
p.putBytes(w, r.imin)
p.putBytes(w, r.imax)
}
return p.err
}
@@ -282,18 +282,18 @@ func (p *sessionRecord) decode(r io.Reader) error {
}
case recCompactionPointer:
level := p.readLevel(br)
key := p.readBytes(br)
ikey := p.readBytes(br)
if p.err == nil {
p.addCompactionPointer(level, iKey(key))
p.addCompactionPointer(level, iKey(ikey))
}
case recNewTable:
level := p.readLevel(br)
num := p.readUvarint(br)
size := p.readUvarint(br)
min := p.readBytes(br)
max := p.readBytes(br)
imin := p.readBytes(br)
imax := p.readBytes(br)
if p.err == nil {
p.addTable(level, num, size, min, max)
p.addTable(level, num, size, imin, imax)
}
case recDeletedTable:
level := p.readLevel(br)
@@ -14,7 +14,7 @@ import (
"github.com/syndtr/goleveldb/leveldb/storage"
)
// logging
// Logging.
type dropper struct {
s *session
@@ -22,22 +22,17 @@ type dropper struct {
}
func (d dropper) Drop(err error) {
if e, ok := err.(journal.DroppedError); ok {
if e, ok := err.(journal.ErrCorrupted); ok {
d.s.logf("journal@drop %s-%d S·%s %q", d.file.Type(), d.file.Num(), shortenb(e.Size), e.Reason)
} else {
d.s.logf("journal@drop %s-%d %q", d.file.Type(), d.file.Num(), err)
}
}
func (s *session) log(v ...interface{}) {
s.stor.Log(fmt.Sprint(v...))
}
func (s *session) log(v ...interface{}) { s.stor.Log(fmt.Sprint(v...)) }
func (s *session) logf(format string, v ...interface{}) { s.stor.Log(fmt.Sprintf(format, v...)) }
func (s *session) logf(format string, v ...interface{}) {
s.stor.Log(fmt.Sprintf(format, v...))
}
// file utils
// File utils.
func (s *session) getJournalFile(num uint64) storage.File {
return s.stor.GetFile(num, storage.TypeJournal)
@@ -56,7 +51,7 @@ func (s *session) newTemp() storage.File {
return s.stor.GetFile(num, storage.TypeTemp)
}
// session state
// Session state.
// Get current version.
func (s *session) version() *version {
@@ -126,7 +121,7 @@ func (s *session) reuseFileNum(num uint64) {
}
}
// manifest related utils
// Manifest related utils.
// Fill given session record obj with current states; need external
// synchronization.
@@ -142,17 +137,17 @@ func (s *session) fillRecord(r *sessionRecord, snapshot bool) {
r.setSeq(s.stSeq)
}
for level, ik := range s.stCPtrs {
for level, ik := range s.stCptrs {
if ik != nil {
r.addCompactionPointer(level, ik)
}
}
r.setComparer(s.cmp.cmp.Name())
r.setComparer(s.icmp.uName())
}
}
// Mark if record has been commited, this will update session state;
// Mark if record has been committed, this will update session state;
// need external synchronization.
func (s *session) recordCommited(r *sessionRecord) {
if r.has(recJournalNum) {
@@ -168,7 +163,7 @@ func (s *session) recordCommited(r *sessionRecord) {
}
for _, p := range r.compactionPointers {
s.stCPtrs[p.level] = iKey(p.key)
s.stCptrs[p.level] = iKey(p.ikey)
}
}
@@ -343,6 +343,20 @@ type fileWrap struct {
f *file
}
func (fw fileWrap) Sync() error {
if err := fw.File.Sync(); err != nil {
return err
}
if fw.f.Type() == TypeManifest {
// Also sync parent directory if file type is manifest.
// See: https://code.google.com/p/leveldb/issues/detail?id=190.
if err := syncDir(fw.f.fs.path); err != nil {
return err
}
}
return nil
}
func (fw fileWrap) Close() error {
f := fw.f
f.fs.mu.Lock()
@@ -38,3 +38,15 @@ func rename(oldpath, newpath string) error {
_, fname := filepath.Split(newpath)
return os.Rename(oldpath, fname)
}
func syncDir(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
if err := f.Sync(); err != nil {
return err
}
return nil
}
@@ -0,0 +1,68 @@
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// +build solaris
package storage
import (
"os"
"syscall"
)
type unixFileLock struct {
f *os.File
}
func (fl *unixFileLock) release() error {
if err := setFileLock(fl.f, false); err != nil {
return err
}
return fl.f.Close()
}
func newFileLock(path string) (fl fileLock, err error) {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return
}
err = setFileLock(f, true)
if err != nil {
f.Close()
return
}
fl = &unixFileLock{f: f}
return
}
func setFileLock(f *os.File, lock bool) error {
flock := syscall.Flock_t{
Type: syscall.F_UNLCK,
Start: 0,
Len: 0,
Whence: 1,
}
if lock {
flock.Type = syscall.F_WRLCK
}
return syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &flock)
}
func rename(oldpath, newpath string) error {
return os.Rename(oldpath, newpath)
}
func syncDir(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
if err := f.Sync(); err != nil {
return err
}
return nil
}
@@ -4,7 +4,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// +build darwin freebsd linux netbsd openbsd
// +build darwin dragonfly freebsd linux netbsd openbsd
package storage
@@ -49,3 +49,15 @@ func setFileLock(f *os.File, lock bool) error {
func rename(oldpath, newpath string) error {
return os.Rename(oldpath, newpath)
}
func syncDir(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
if err := f.Sync(); err != nil {
return err
}
return nil
}
@@ -65,3 +65,5 @@ func rename(oldpath, newpath string) error {
}
return moveFileEx(from, to, _MOVEFILE_REPLACE_EXISTING)
}
func syncDir(name string) error { return nil }
@@ -9,6 +9,7 @@ package storage
import (
"errors"
"fmt"
"io"
"github.com/syndtr/goleveldb/leveldb/util"
@@ -36,7 +37,7 @@ func (t FileType) String() string {
case TypeTemp:
return "temp"
}
return "<unknown>"
return fmt.Sprintf("<unknown:%d>", t)
}
var (
@@ -66,7 +67,7 @@ type Writer interface {
Syncer
}
// File is the file.
// File is the file. A file instance must be goroutine-safe.
type File interface {
// Open opens the file for read. Returns os.ErrNotExist error
// if the file does not exist.
@@ -93,7 +94,7 @@ type File interface {
Remove() error
}
// Storage is the storage.
// Storage is the storage. A storage instance must be goroutine-safe.
type Storage interface {
// Lock locks the storage. Any subsequent attempt to call Lock will fail
// until the last lock released.
@@ -20,7 +20,7 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
const typeShift = 3
const typeShift = 4
var (
tsErrInvalidFile = errors.New("leveldb.testStorage: invalid file for argument")
@@ -7,11 +7,12 @@
package leveldb
import (
"fmt"
"sort"
"sync"
"sync/atomic"
"github.com/syndtr/goleveldb/leveldb/cache"
"github.com/syndtr/goleveldb/leveldb/comparer"
"github.com/syndtr/goleveldb/leveldb/iterator"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/storage"
@@ -19,34 +20,41 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
// table file
// tFile holds basic information about a table.
type tFile struct {
file storage.File
seekLeft int32
size uint64
min, max iKey
file storage.File
seekLeft int32
size uint64
imin, imax iKey
}
// test if key is after t
func (t *tFile) isAfter(key []byte, ucmp comparer.BasicComparer) bool {
return key != nil && ucmp.Compare(key, t.max.ukey()) > 0
// Returns true if given key is after largest key of this table.
func (t *tFile) after(icmp *iComparer, ukey []byte) bool {
return ukey != nil && icmp.uCompare(ukey, t.imax.ukey()) > 0
}
// test if key is before t
func (t *tFile) isBefore(key []byte, ucmp comparer.BasicComparer) bool {
return key != nil && ucmp.Compare(key, t.min.ukey()) < 0
// Returns true if given key is before smallest key of this table.
func (t *tFile) before(icmp *iComparer, ukey []byte) bool {
return ukey != nil && icmp.uCompare(ukey, t.imin.ukey()) < 0
}
func (t *tFile) incrSeek() int32 {
// Returns true if given key range overlaps with this table key range.
func (t *tFile) overlaps(icmp *iComparer, umin, umax []byte) bool {
return !t.after(icmp, umin) && !t.before(icmp, umax)
}
// Cosumes one seek and return current seeks left.
func (t *tFile) consumeSeek() int32 {
return atomic.AddInt32(&t.seekLeft, -1)
}
func newTFile(file storage.File, size uint64, min, max iKey) *tFile {
// Creates new tFile.
func newTableFile(file storage.File, size uint64, imin, imax iKey) *tFile {
f := &tFile{
file: file,
size: size,
min: min,
max: max,
imin: imin,
imax: imax,
}
// We arrange to automatically compact this file after
@@ -70,33 +78,40 @@ func newTFile(file storage.File, size uint64, min, max iKey) *tFile {
return f
}
// table files
// tFiles hold multiple tFile.
type tFiles []*tFile
func (tf tFiles) Len() int { return len(tf) }
func (tf tFiles) Swap(i, j int) { tf[i], tf[j] = tf[j], tf[i] }
// Returns true if i smallest key is less than j.
// This used for sort by key in ascending order.
func (tf tFiles) lessByKey(icmp *iComparer, i, j int) bool {
a, b := tf[i], tf[j]
n := icmp.Compare(a.min, b.min)
n := icmp.Compare(a.imin, b.imin)
if n == 0 {
return a.file.Num() < b.file.Num()
}
return n < 0
}
// Returns true if i file number is greater than j.
// This used for sort by file number in descending order.
func (tf tFiles) lessByNum(i, j int) bool {
return tf[i].file.Num() > tf[j].file.Num()
}
// Sorts tables by key in ascending order.
func (tf tFiles) sortByKey(icmp *iComparer) {
sort.Sort(&tFilesSortByKey{tFiles: tf, icmp: icmp})
}
// Sorts tables by file number in descending order.
func (tf tFiles) sortByNum() {
sort.Sort(&tFilesSortByNum{tFiles: tf})
}
// Returns sum of all tables size.
func (tf tFiles) size() (sum uint64) {
for _, t := range tf {
sum += t.size
@@ -104,96 +119,106 @@ func (tf tFiles) size() (sum uint64) {
return sum
}
func (tf tFiles) searchMin(key iKey, icmp *iComparer) int {
// Searches smallest index of tables whose its smallest
// key is after or equal with given key.
func (tf tFiles) searchMin(icmp *iComparer, ikey iKey) int {
return sort.Search(len(tf), func(i int) bool {
return icmp.Compare(tf[i].min, key) >= 0
return icmp.Compare(tf[i].imin, ikey) >= 0
})
}
func (tf tFiles) searchMax(key iKey, icmp *iComparer) int {
// Searches smallest index of tables whose its largest
// key is after or equal with given key.
func (tf tFiles) searchMax(icmp *iComparer, ikey iKey) int {
return sort.Search(len(tf), func(i int) bool {
return icmp.Compare(tf[i].max, key) >= 0
return icmp.Compare(tf[i].imax, ikey) >= 0
})
}
func (tf tFiles) isOverlaps(min, max []byte, disjSorted bool, icmp *iComparer) bool {
ucmp := icmp.cmp
if !disjSorted {
// Need to check against all files
// Returns true if given key range overlaps with one or more
// tables key range. If unsorted is true then binary search will not be used.
func (tf tFiles) overlaps(icmp *iComparer, umin, umax []byte, unsorted bool) bool {
if unsorted {
// Check against all files.
for _, t := range tf {
if !t.isAfter(min, ucmp) && !t.isBefore(max, ucmp) {
if t.overlaps(icmp, umin, umax) {
return true
}
}
return false
}
var idx int
if len(min) > 0 {
// Find the earliest possible internal key for min
idx = tf.searchMax(newIKey(min, kMaxSeq, tSeek), icmp)
i := 0
if len(umin) > 0 {
// Find the earliest possible internal key for min.
i = tf.searchMax(icmp, newIKey(umin, kMaxSeq, tSeek))
}
if idx >= len(tf) {
// beginning of range is after all files, so no overlap
if i >= len(tf) {
// Beginning of range is after all files, so no overlap.
return false
}
return !tf[idx].isBefore(max, ucmp)
return !tf[i].before(icmp, umax)
}
func (tf tFiles) getOverlaps(min, max []byte, r *tFiles, disjSorted bool, ucmp comparer.BasicComparer) {
// Returns tables whose its key range overlaps with given key range.
// If overlapped is true then the search will be expanded to tables that
// overlaps with each other.
func (tf tFiles) getOverlaps(dst tFiles, icmp *iComparer, umin, umax []byte, overlapped bool) tFiles {
x := len(dst)
for i := 0; i < len(tf); {
t := tf[i]
i++
if t.isAfter(min, ucmp) || t.isBefore(max, ucmp) {
continue
}
*r = append(*r, t)
if !disjSorted {
// Level-0 files may overlap each other. So check if the newly
// added file has expanded the range. If so, restart search.
if min != nil && ucmp.Compare(t.min.ukey(), min) < 0 {
min = t.min.ukey()
*r = nil
i = 0
} else if max != nil && ucmp.Compare(t.max.ukey(), max) > 0 {
max = t.max.ukey()
*r = nil
i = 0
if t.overlaps(icmp, umin, umax) {
if overlapped {
// For overlapped files, check if the newly added file has
// expanded the range. If so, restart search.
if umin != nil && icmp.uCompare(t.imin.ukey(), umin) < 0 {
umin = t.imin.ukey()
dst = dst[:x]
i = 0
continue
} else if umax != nil && icmp.uCompare(t.imax.ukey(), umax) > 0 {
umax = t.imax.ukey()
dst = dst[:x]
i = 0
continue
}
}
dst = append(dst, t)
}
i++
}
return
return dst
}
func (tf tFiles) getRange(icmp *iComparer) (min, max iKey) {
// Returns tables key range.
func (tf tFiles) getRange(icmp *iComparer) (imin, imax iKey) {
for i, t := range tf {
if i == 0 {
min, max = t.min, t.max
imin, imax = t.imin, t.imax
continue
}
if icmp.Compare(t.min, min) < 0 {
min = t.min
if icmp.Compare(t.imin, imin) < 0 {
imin = t.imin
}
if icmp.Compare(t.max, max) > 0 {
max = t.max
if icmp.Compare(t.imax, imax) > 0 {
imax = t.imax
}
}
return
}
// Creates iterator index from tables.
func (tf tFiles) newIndexIterator(tops *tOps, icmp *iComparer, slice *util.Range, ro *opt.ReadOptions) iterator.IteratorIndexer {
if slice != nil {
var start, limit int
if slice.Start != nil {
start = tf.searchMax(iKey(slice.Start), icmp)
start = tf.searchMax(icmp, iKey(slice.Start))
}
if slice.Limit != nil {
limit = tf.searchMin(iKey(slice.Limit), icmp)
limit = tf.searchMin(icmp, iKey(slice.Limit))
} else {
limit = tf.Len()
}
@@ -208,6 +233,7 @@ func (tf tFiles) newIndexIterator(tops *tOps, icmp *iComparer, slice *util.Range
})
}
// Tables iterator index.
type tFilesArrayIndexer struct {
tFiles
tops *tOps
@@ -217,7 +243,7 @@ type tFilesArrayIndexer struct {
}
func (a *tFilesArrayIndexer) Search(key []byte) int {
return a.searchMax(iKey(key), a.icmp)
return a.searchMax(a.icmp, iKey(key))
}
func (a *tFilesArrayIndexer) Get(i int) iterator.Iterator {
@@ -227,6 +253,7 @@ func (a *tFilesArrayIndexer) Get(i int) iterator.Iterator {
return a.tops.newIterator(a.tFiles[i], nil, a.ro)
}
// Helper type for sortByKey.
type tFilesSortByKey struct {
tFiles
icmp *iComparer
@@ -236,6 +263,7 @@ func (x *tFilesSortByKey) Less(i, j int) bool {
return x.lessByKey(x.icmp, i, j)
}
// Helper type for sortByNum.
type tFilesSortByNum struct {
tFiles
}
@@ -244,19 +272,17 @@ func (x *tFilesSortByNum) Less(i, j int) bool {
return x.lessByNum(i, j)
}
// table operations
// Table operations.
type tOps struct {
s *session
cache cache.Cache
cacheNS cache.Namespace
bpool *util.BufferPool
mu sync.Mutex
closed bool
}
func newTableOps(s *session, cacheCap int) *tOps {
c := cache.NewLRUCache(cacheCap)
ns := c.GetNamespace(0)
return &tOps{s, c, ns}
}
// Creates an empty table and returns table writer.
func (t *tOps) create() (*tWriter, error) {
file := t.s.getTableFile(t.s.allocFileNum())
fw, err := file.Create()
@@ -271,10 +297,11 @@ func (t *tOps) create() (*tWriter, error) {
}, nil
}
// Builds table from src iterator.
func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
w, err := t.create()
if err != nil {
return f, n, err
return
}
defer func() {
@@ -284,7 +311,7 @@ func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
}()
for src.Next() {
err = w.add(src.Key(), src.Value())
err = w.append(src.Key(), src.Value())
if err != nil {
return
}
@@ -299,84 +326,142 @@ func (t *tOps) createFrom(src iterator.Iterator) (f *tFile, n int, err error) {
return
}
func (t *tOps) lookup(f *tFile) (c cache.Object, err error) {
type trWrapper struct {
*table.Reader
t *tOps
ref int
}
func (w *trWrapper) Release() {
if w.ref != 0 && !w.t.closed {
panic(fmt.Sprintf("BUG: invalid ref %d, refer to issue #72", w.ref))
}
w.Reader.Release()
}
type trCacheHandleWrapper struct {
cache.Handle
t *tOps
released bool
}
func (w *trCacheHandleWrapper) Release() {
w.t.mu.Lock()
defer w.t.mu.Unlock()
if !w.released {
w.released = true
w.Value().(*trWrapper).ref--
}
w.Handle.Release()
}
// Opens table. It returns a cache handle, which should
// be released after use.
func (t *tOps) open(f *tFile) (ch cache.Handle, err error) {
t.mu.Lock()
defer t.mu.Unlock()
num := f.file.Num()
c, ok := t.cacheNS.Get(num, func() (ok bool, value interface{}, charge int, fin cache.SetFin) {
ch = t.cacheNS.Get(num, func() (charge int, value interface{}) {
var r storage.Reader
r, err = f.file.Open()
if err != nil {
return
return 0, nil
}
o := t.s.o
var cacheNS cache.Namespace
if bc := o.GetBlockCache(); bc != nil {
cacheNS = bc.GetNamespace(num)
var bcacheNS cache.Namespace
if bc := t.s.o.GetBlockCache(); bc != nil {
bcacheNS = bc.GetNamespace(num)
}
ok = true
value = table.NewReader(r, int64(f.size), cacheNS, o)
charge = 1
fin = func() {
r.Close()
}
return
return 1, &trWrapper{table.NewReader(r, int64(f.size), bcacheNS, t.bpool, t.s.o), t, 0}
})
if !ok && err == nil {
if ch == nil && err == nil {
err = ErrClosed
}
ch.Value().(*trWrapper).ref++
ch = &trCacheHandleWrapper{ch, t, false}
return
}
func (t *tOps) get(f *tFile, key []byte, ro *opt.ReadOptions) (rkey, rvalue []byte, err error) {
c, err := t.lookup(f)
// Finds key/value pair whose key is greater than or equal to the
// given key.
func (t *tOps) find(f *tFile, key []byte, ro *opt.ReadOptions) (rkey, rvalue []byte, err error) {
ch, err := t.open(f)
if err != nil {
return nil, nil, err
}
defer c.Release()
return c.Value().(*table.Reader).Find(key, ro)
defer ch.Release()
return ch.Value().(*trWrapper).Find(key, ro)
}
func (t *tOps) getApproximateOffset(f *tFile, key []byte) (offset uint64, err error) {
c, err := t.lookup(f)
// Returns approximate offset of the given key.
func (t *tOps) offsetOf(f *tFile, key []byte) (offset uint64, err error) {
ch, err := t.open(f)
if err != nil {
return
}
_offset, err := c.Value().(*table.Reader).GetApproximateOffset(key)
offset = uint64(_offset)
c.Release()
return
defer ch.Release()
offset_, err := ch.Value().(*trWrapper).OffsetOf(key)
return uint64(offset_), err
}
// Creates an iterator from the given table.
func (t *tOps) newIterator(f *tFile, slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
c, err := t.lookup(f)
ch, err := t.open(f)
if err != nil {
return iterator.NewEmptyIterator(err)
}
iter := c.Value().(*table.Reader).NewIterator(slice, ro)
iter.SetReleaser(c)
iter := ch.Value().(*trWrapper).NewIterator(slice, ro)
iter.SetReleaser(ch)
return iter
}
// Removes table from persistent storage. It waits until
// no one use the the table.
func (t *tOps) remove(f *tFile) {
t.mu.Lock()
defer t.mu.Unlock()
num := f.file.Num()
t.cacheNS.Delete(num, func(exist bool) {
if err := f.file.Remove(); err != nil {
t.s.logf("table@remove removing @%d %q", num, err)
} else {
t.s.logf("table@remove removed @%d", num)
}
if bc := t.s.o.GetBlockCache(); bc != nil {
bc.GetNamespace(num).Zap(false)
t.cacheNS.Delete(num, func(exist, pending bool) {
if !pending {
if err := f.file.Remove(); err != nil {
t.s.logf("table@remove removing @%d %q", num, err)
} else {
t.s.logf("table@remove removed @%d", num)
}
if bc := t.s.o.GetBlockCache(); bc != nil {
bc.ZapNamespace(num)
}
}
})
}
// Closes the table ops instance. It will close all tables,
// regadless still used or not.
func (t *tOps) close() {
t.cache.Zap(true)
t.mu.Lock()
defer t.mu.Unlock()
t.closed = true
t.cache.Zap()
t.bpool.Close()
}
// Creates new initialized table ops instance.
func newTableOps(s *session, cacheCap int) *tOps {
c := cache.NewLRUCache(cacheCap)
return &tOps{
s: s,
cache: c,
cacheNS: c.GetNamespace(0),
bpool: util.NewBufferPool(s.o.GetBlockSize() + 5),
}
}
// tWriter wraps the table writer. It keep track of file descriptor
// and added key range.
type tWriter struct {
t *tOps
@@ -387,7 +472,8 @@ type tWriter struct {
first, last []byte
}
func (w *tWriter) add(key, value []byte) error {
// Append key/value pair to the table.
func (w *tWriter) append(key, value []byte) error {
if w.first == nil {
w.first = append([]byte{}, key...)
}
@@ -395,10 +481,12 @@ func (w *tWriter) add(key, value []byte) error {
return w.tw.Append(key, value)
}
// Returns true if the table is empty.
func (w *tWriter) empty() bool {
return w.first == nil
}
// Finalizes the table and returns table file.
func (w *tWriter) finish() (f *tFile, err error) {
err = w.tw.Close()
if err != nil {
@@ -410,10 +498,11 @@ func (w *tWriter) finish() (f *tFile, err error) {
return
}
w.w.Close()
f = newTFile(w.file, uint64(w.tw.BytesLen()), iKey(w.first), iKey(w.last))
f = newTableFile(w.file, uint64(w.tw.BytesLen()), iKey(w.first), iKey(w.last))
return
}
// Drops the table.
func (w *tWriter) drop() {
w.w.Close()
w.file.Remove()
@@ -40,7 +40,7 @@ var _ = testutil.Defer(func() {
data := bw.buf.Bytes()
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
return &block{
cmp: comparer.DefaultComparer,
tr: &Reader{cmp: comparer.DefaultComparer},
data: data,
restartsLen: restartsLen,
restartsOffset: len(data) - (restartsLen+1)*4,
@@ -59,7 +59,7 @@ var _ = testutil.Defer(func() {
// Make block.
br := Build(kv, restartInterval)
// Do testing.
testutil.KeyValueTesting(nil, br, kv.Clone())
testutil.KeyValueTesting(nil, kv.Clone(), br, nil, nil)
}
Describe(Text(), Test)
@@ -13,6 +13,7 @@ import (
"io"
"sort"
"strings"
"sync"
"code.google.com/p/snappy-go/snappy"
@@ -25,8 +26,9 @@ import (
)
var (
ErrNotFound = util.ErrNotFound
ErrIterReleased = errors.New("leveldb/table: iterator released")
ErrNotFound = util.ErrNotFound
ErrReaderReleased = errors.New("leveldb/table: reader released")
ErrIterReleased = errors.New("leveldb/table: iterator released")
)
func max(x, y int) int {
@@ -37,7 +39,7 @@ func max(x, y int) int {
}
type block struct {
cmp comparer.BasicComparer
tr *Reader
data []byte
restartsLen int
restartsOffset int
@@ -46,31 +48,25 @@ type block struct {
}
func (b *block) seek(rstart, rlimit int, key []byte) (index, offset int, err error) {
n := b.restartsOffset
data := b.data
cmp := b.cmp
index = sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
offset := int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):]))
offset += 1 // shared always zero, since this is a restart point
v1, n1 := binary.Uvarint(data[offset:]) // key length
_, n2 := binary.Uvarint(data[offset+n1:]) // value length
offset := int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):]))
offset += 1 // shared always zero, since this is a restart point
v1, n1 := binary.Uvarint(b.data[offset:]) // key length
_, n2 := binary.Uvarint(b.data[offset+n1:]) // value length
m := offset + n1 + n2
return cmp.Compare(data[m:m+int(v1)], key) > 0
return b.tr.cmp.Compare(b.data[m:m+int(v1)], key) > 0
}) + rstart - 1
if index < rstart {
// The smallest key is greater-than key sought.
index = rstart
}
offset = int(binary.LittleEndian.Uint32(data[n+4*index:]))
offset = int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*index:]))
return
}
func (b *block) restartIndex(rstart, rlimit, offset int) int {
n := b.restartsOffset
data := b.data
return sort.Search(b.restartsLen-rstart-(b.restartsLen-rlimit), func(i int) bool {
return int(binary.LittleEndian.Uint32(data[n+4*(rstart+i):])) > offset
return int(binary.LittleEndian.Uint32(b.data[b.restartsOffset+4*(rstart+i):])) > offset
}) + rstart - 1
}
@@ -139,6 +135,12 @@ func (b *block) newIterator(slice *util.Range, inclLimit bool, cache util.Releas
return bi
}
func (b *block) Release() {
b.tr.bpool.Put(b.data)
b.tr = nil
b.data = nil
}
type dir int
const (
@@ -261,7 +263,7 @@ func (i *blockIter) Seek(key []byte) bool {
i.dir = dirForward
}
for i.Next() {
if i.block.cmp.Compare(i.key, key) >= 0 {
if i.block.tr.cmp.Compare(i.key, key) >= 0 {
return true
}
}
@@ -437,25 +439,32 @@ func (i *blockIter) Value() []byte {
}
func (i *blockIter) Release() {
i.prevNode = nil
i.prevKeys = nil
i.key = nil
i.value = nil
i.dir = dirReleased
if i.cache != nil {
i.cache.Release()
i.cache = nil
}
if i.releaser != nil {
i.releaser.Release()
i.releaser = nil
if i.dir != dirReleased {
i.block = nil
i.prevNode = nil
i.prevKeys = nil
i.key = nil
i.value = nil
i.dir = dirReleased
if i.cache != nil {
i.cache.Release()
i.cache = nil
}
if i.releaser != nil {
i.releaser.Release()
i.releaser = nil
}
}
}
func (i *blockIter) SetReleaser(releaser util.Releaser) {
if i.dir > dirReleased {
i.releaser = releaser
if i.dir == dirReleased {
panic(util.ErrReleased)
}
if i.releaser != nil && releaser != nil {
panic(util.ErrHasReleaser)
}
i.releaser = releaser
}
func (i *blockIter) Valid() bool {
@@ -467,7 +476,7 @@ func (i *blockIter) Error() error {
}
type filterBlock struct {
filter filter.Filter
tr *Reader
data []byte
oOffset int
baseLg uint
@@ -481,7 +490,7 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
n := int(binary.LittleEndian.Uint32(o))
m := int(binary.LittleEndian.Uint32(o[4:]))
if n < m && m <= b.oOffset {
return b.filter.Contains(b.data[n:m], key)
return b.tr.filter.Contains(b.data[n:m], key)
} else if n == m {
return false
}
@@ -489,10 +498,15 @@ func (b *filterBlock) contains(offset uint64, key []byte) bool {
return true
}
func (b *filterBlock) Release() {
b.tr.bpool.Put(b.data)
b.tr = nil
b.data = nil
}
type indexIter struct {
blockIter
tableReader *Reader
slice *util.Range
*blockIter
slice *util.Range
// Options
checksum bool
fillCache bool
@@ -507,27 +521,31 @@ func (i *indexIter) Get() iterator.Iterator {
if n == 0 {
return iterator.NewEmptyIterator(errors.New("leveldb/table: Reader: invalid table (bad data block handle)"))
}
var slice *util.Range
if i.slice != nil && (i.blockIter.isFirst() || i.blockIter.isLast()) {
slice = i.slice
}
return i.tableReader.getDataIter(dataBH, slice, i.checksum, i.fillCache)
return i.blockIter.block.tr.getDataIterErr(dataBH, slice, i.checksum, i.fillCache)
}
// Reader is a table reader.
type Reader struct {
mu sync.RWMutex
reader io.ReaderAt
cache cache.Namespace
err error
bpool *util.BufferPool
// Options
cmp comparer.Comparer
filter filter.Filter
checksum bool
strictIter bool
dataEnd int64
indexBlock *block
filterBlock *filterBlock
dataEnd int64
indexBH, filterBH blockHandle
indexBlock *block
filterBlock *filterBlock
}
func verifyChecksum(data []byte) bool {
@@ -538,12 +556,13 @@ func verifyChecksum(data []byte) bool {
}
func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
data := make([]byte, bh.length+blockTrailerLen)
data := r.bpool.Get(int(bh.length + blockTrailerLen))
if _, err := r.reader.ReadAt(data, int64(bh.offset)); err != nil && err != io.EOF {
return nil, err
}
if checksum || r.checksum {
if !verifyChecksum(data) {
r.bpool.Put(data)
return nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
}
}
@@ -551,12 +570,18 @@ func (r *Reader) readRawBlock(bh blockHandle, checksum bool) ([]byte, error) {
case blockTypeNoCompression:
data = data[:bh.length]
case blockTypeSnappyCompression:
var err error
data, err = snappy.Decode(nil, data[:bh.length])
decLen, err := snappy.DecodedLen(data[:bh.length])
if err != nil {
return nil, err
}
tmp := data
data, err = snappy.Decode(r.bpool.Get(decLen), tmp[:bh.length])
r.bpool.Put(tmp)
if err != nil {
return nil, err
}
default:
r.bpool.Put(data)
return nil, fmt.Errorf("leveldb/table: Reader: unknown block compression type: %d", data[bh.length])
}
return data, nil
@@ -569,7 +594,7 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
}
restartsLen := int(binary.LittleEndian.Uint32(data[len(data)-4:]))
b := &block{
cmp: r.cmp,
tr: r,
data: data,
restartsLen: restartsLen,
restartsOffset: len(data) - (restartsLen+1)*4,
@@ -578,7 +603,44 @@ func (r *Reader) readBlock(bh blockHandle, checksum bool) (*block, error) {
return b, nil
}
func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterBlock, error) {
func (r *Reader) readBlockCached(bh blockHandle, checksum, fillCache bool) (*block, util.Releaser, error) {
if r.cache != nil {
var err error
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
if !fillCache {
return 0, nil
}
var b *block
b, err = r.readBlock(bh, checksum)
if err != nil {
return 0, nil
}
return cap(b.data), b
})
if ch != nil {
b, ok := ch.Value().(*block)
if !ok {
ch.Release()
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
}
if !b.checksum && (r.checksum || checksum) {
if !verifyChecksum(b.data) {
ch.Release()
return nil, nil, errors.New("leveldb/table: Reader: invalid block (checksum mismatch)")
}
b.checksum = true
}
return b, ch, err
} else if err != nil {
return nil, nil, err
}
}
b, err := r.readBlock(bh, checksum)
return b, b, err
}
func (r *Reader) readFilterBlock(bh blockHandle) (*filterBlock, error) {
data, err := r.readRawBlock(bh, true)
if err != nil {
return nil, err
@@ -593,7 +655,7 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
return nil, errors.New("leveldb/table: Reader: invalid filter block (invalid offset)")
}
b := &filterBlock{
filter: filter,
tr: r,
data: data,
oOffset: oOffset,
baseLg: uint(data[n-1]),
@@ -602,44 +664,67 @@ func (r *Reader) readFilterBlock(bh blockHandle, filter filter.Filter) (*filterB
return b, nil
}
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
func (r *Reader) readFilterBlockCached(bh blockHandle, fillCache bool) (*filterBlock, util.Releaser, error) {
if r.cache != nil {
// Get/set block cache.
var err error
cache, ok := r.cache.Get(dataBH.offset, func() (ok bool, value interface{}, charge int, fin cache.SetFin) {
ch := r.cache.Get(bh.offset, func() (charge int, value interface{}) {
if !fillCache {
return
return 0, nil
}
var dataBlock *block
dataBlock, err = r.readBlock(dataBH, checksum)
if err == nil {
ok = true
value = dataBlock
charge = int(dataBH.length)
var b *filterBlock
b, err = r.readFilterBlock(bh)
if err != nil {
return 0, nil
}
return
return cap(b.data), b
})
if err != nil {
return iterator.NewEmptyIterator(err)
}
if ok {
dataBlock := cache.Value().(*block)
if !dataBlock.checksum && (r.checksum || checksum) {
if !verifyChecksum(dataBlock.data) {
return iterator.NewEmptyIterator(errors.New("leveldb/table: Reader: invalid block (checksum mismatch)"))
}
dataBlock.checksum = true
if ch != nil {
b, ok := ch.Value().(*filterBlock)
if !ok {
ch.Release()
return nil, nil, errors.New("leveldb/table: Reader: inconsistent block type")
}
iter := dataBlock.newIterator(slice, false, cache)
return iter
return b, ch, err
} else if err != nil {
return nil, nil, err
}
}
dataBlock, err := r.readBlock(dataBH, checksum)
b, err := r.readFilterBlock(bh)
return b, b, err
}
func (r *Reader) getIndexBlock(fillCache bool) (b *block, rel util.Releaser, err error) {
if r.indexBlock == nil {
return r.readBlockCached(r.indexBH, true, fillCache)
}
return r.indexBlock, util.NoopReleaser{}, nil
}
func (r *Reader) getFilterBlock(fillCache bool) (*filterBlock, util.Releaser, error) {
if r.filterBlock == nil {
return r.readFilterBlockCached(r.filterBH, fillCache)
}
return r.filterBlock, util.NoopReleaser{}, nil
}
func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
b, rel, err := r.readBlockCached(dataBH, checksum, fillCache)
if err != nil {
return iterator.NewEmptyIterator(err)
}
iter := dataBlock.newIterator(slice, false, nil)
return iter
return b.newIterator(slice, false, rel)
}
func (r *Reader) getDataIterErr(dataBH blockHandle, slice *util.Range, checksum, fillCache bool) iterator.Iterator {
r.mu.RLock()
defer r.mu.RUnlock()
if r.err != nil {
return iterator.NewEmptyIterator(r.err)
}
return r.getDataIter(dataBH, slice, checksum, fillCache)
}
// NewIterator creates an iterator from the table.
@@ -653,20 +738,26 @@ func (r *Reader) getDataIter(dataBH blockHandle, slice *util.Range, checksum, fi
// when not used.
//
// Also read Iterator documentation of the leveldb/iterator package.
func (r *Reader) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.Iterator {
r.mu.RLock()
defer r.mu.RUnlock()
if r.err != nil {
return iterator.NewEmptyIterator(r.err)
}
index := &indexIter{
blockIter: *r.indexBlock.newIterator(slice, true, nil),
tableReader: r,
slice: slice,
checksum: ro.GetStrict(opt.StrictBlockChecksum),
fillCache: !ro.GetDontFillCache(),
fillCache := !ro.GetDontFillCache()
indexBlock, rel, err := r.getIndexBlock(fillCache)
if err != nil {
return iterator.NewEmptyIterator(err)
}
return iterator.NewIndexedIterator(index, r.strictIter || ro.GetStrict(opt.StrictIterator), false)
index := &indexIter{
blockIter: indexBlock.newIterator(slice, true, rel),
slice: slice,
checksum: ro.GetStrict(opt.StrictBlockChecksum),
fillCache: !ro.GetDontFillCache(),
}
return iterator.NewIndexedIterator(index, r.strictIter || ro.GetStrict(opt.StrictIterator), true)
}
// Find finds key/value pair whose key is greater than or equal to the
@@ -676,12 +767,21 @@ func (r *Reader) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.It
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Find returns.
func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err error) {
r.mu.RLock()
defer r.mu.RUnlock()
if r.err != nil {
err = r.err
return
}
index := r.indexBlock.newIterator(nil, true, nil)
indexBlock, rel, err := r.getIndexBlock(true)
if err != nil {
return
}
defer rel.Release()
index := indexBlock.newIterator(nil, true, nil)
defer index.Release()
if !index.Seek(key) {
err = index.Error()
@@ -695,9 +795,15 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
err = errors.New("leveldb/table: Reader: invalid table (bad data block handle)")
return
}
if r.filterBlock != nil && !r.filterBlock.contains(dataBH.offset, key) {
err = ErrNotFound
return
if r.filter != nil {
filterBlock, rel, ferr := r.getFilterBlock(true)
if ferr == nil {
if !filterBlock.contains(dataBH.offset, key) {
rel.Release()
return nil, nil, ErrNotFound
}
rel.Release()
}
}
data := r.getDataIter(dataBH, nil, ro.GetStrict(opt.StrictBlockChecksum), !ro.GetDontFillCache())
defer data.Release()
@@ -708,8 +814,15 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
}
return
}
// Don't use block buffer, no need to copy the buffer.
rkey = data.Key()
value = data.Value()
if r.bpool == nil {
value = data.Value()
} else {
// Use block buffer, and since the buffer will be recycled, the buffer
// need to be copied.
value = append([]byte{}, data.Value()...)
}
return
}
@@ -719,6 +832,9 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Get returns.
func (r *Reader) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
r.mu.RLock()
defer r.mu.RUnlock()
if r.err != nil {
err = r.err
return
@@ -732,16 +848,25 @@ func (r *Reader) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error)
return
}
// GetApproximateOffset returns approximate offset for the given key.
// OffsetOf returns approximate offset for the given key.
//
// It is safe to modify the contents of the argument after Get returns.
func (r *Reader) GetApproximateOffset(key []byte) (offset int64, err error) {
func (r *Reader) OffsetOf(key []byte) (offset int64, err error) {
r.mu.RLock()
defer r.mu.RUnlock()
if r.err != nil {
err = r.err
return
}
index := r.indexBlock.newIterator(nil, true, nil)
indexBlock, rel, err := r.readBlockCached(r.indexBH, true, true)
if err != nil {
return
}
defer rel.Release()
index := indexBlock.newIterator(nil, true, nil)
defer index.Release()
if index.Seek(key) {
dataBH, n := decodeBlockHandle(index.Value())
@@ -759,12 +884,38 @@ func (r *Reader) GetApproximateOffset(key []byte) (offset int64, err error) {
return
}
// Release implements util.Releaser.
// It also close the file if it is an io.Closer.
func (r *Reader) Release() {
r.mu.Lock()
defer r.mu.Unlock()
if closer, ok := r.reader.(io.Closer); ok {
closer.Close()
}
if r.indexBlock != nil {
r.indexBlock.Release()
r.indexBlock = nil
}
if r.filterBlock != nil {
r.filterBlock.Release()
r.filterBlock = nil
}
r.reader = nil
r.cache = nil
r.bpool = nil
r.err = ErrReaderReleased
}
// NewReader creates a new initialized table reader for the file.
// The cache is optional and can be nil.
func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, o *opt.Options) *Reader {
// The cache and bpool is optional and can be nil.
//
// The returned table reader instance is goroutine-safe.
func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, bpool *util.BufferPool, o *opt.Options) *Reader {
r := &Reader{
reader: f,
cache: cache,
bpool: bpool,
cmp: o.GetComparer(),
checksum: o.GetStrict(opt.StrictBlockChecksum),
strictIter: o.GetStrict(opt.StrictIterator),
@@ -792,16 +943,11 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, o *opt.Options)
return r
}
// Decode the index block handle.
indexBH, n := decodeBlockHandle(footer[n:])
r.indexBH, n = decodeBlockHandle(footer[n:])
if n == 0 {
r.err = errors.New("leveldb/table: Reader: invalid table (bad index block handle)")
return r
}
// Read index block.
r.indexBlock, r.err = r.readBlock(indexBH, true)
if r.err != nil {
return r
}
// Read metaindex block.
metaBlock, err := r.readBlock(metaBH, true)
if err != nil {
@@ -817,32 +963,45 @@ func NewReader(f io.ReaderAt, size int64, cache cache.Namespace, o *opt.Options)
continue
}
fn := key[7:]
var filter filter.Filter
if f0 := o.GetFilter(); f0 != nil && f0.Name() == fn {
filter = f0
r.filter = f0
} else {
for _, f0 := range o.GetAltFilters() {
if f0.Name() == fn {
filter = f0
r.filter = f0
break
}
}
}
if filter != nil {
if r.filter != nil {
filterBH, n := decodeBlockHandle(metaIter.Value())
if n == 0 {
continue
}
r.filterBH = filterBH
// Update data end.
r.dataEnd = int64(filterBH.offset)
filterBlock, err := r.readFilterBlock(filterBH, filter)
if err != nil {
continue
}
r.filterBlock = filterBlock
break
}
}
metaIter.Release()
metaBlock.Release()
// Cache index and filter block locally, since we don't have global cache.
if cache == nil {
r.indexBlock, r.err = r.readBlock(r.indexBH, true)
if r.err != nil {
return r
}
if r.filter != nil {
r.filterBlock, err = r.readFilterBlock(r.filterBH)
if err != nil {
// Don't use filter then.
r.filter = nil
r.filterBH = blockHandle{}
}
}
}
return r
}
@@ -59,9 +59,9 @@ var _ = testutil.Defer(func() {
It("Should be able to approximate offset of a key correctly", func() {
Expect(err).ShouldNot(HaveOccurred())
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, o)
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, nil, o)
CheckOffset := func(key string, expect, threshold int) {
offset, err := tr.GetApproximateOffset([]byte(key))
offset, err := tr.OffsetOf([]byte(key))
Expect(err).ShouldNot(HaveOccurred())
Expect(offset).Should(BeNumerically("~", expect, threshold), "Offset of key %q", key)
}
@@ -95,7 +95,7 @@ var _ = testutil.Defer(func() {
tw.Close()
// Opening the table.
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, o)
tr := NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()), nil, nil, o)
return tableWrapper{tr}
}
Test := func(kv *testutil.KeyValue, body func(r *Reader)) func() {
@@ -104,14 +104,16 @@ var _ = testutil.Defer(func() {
if body != nil {
body(db.(tableWrapper).Reader)
}
testutil.KeyValueTesting(nil, db, *kv)
testutil.KeyValueTesting(nil, *kv, db, nil, nil)
}
}
testutil.AllKeyValueTesting(nil, Build)
testutil.AllKeyValueTesting(nil, Build, nil, nil)
Describe("with one key per block", Test(testutil.KeyValue_Generate(nil, 9, 1, 10, 512, 512), func(r *Reader) {
It("should have correct blocks number", func() {
Expect(r.indexBlock.restartsLen).Should(Equal(9))
indexBlock, err := r.readBlock(r.indexBH, true)
Expect(err).To(BeNil())
Expect(indexBlock.restartsLen).Should(Equal(9))
})
}))
})
@@ -16,13 +16,22 @@ import (
"github.com/syndtr/goleveldb/leveldb/util"
)
func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB, teardown func(DB)) {
if rnd == nil {
rnd = NewRand()
}
if db, ok := p.(Find); ok {
It("Should find all keys with Find", func() {
if p == nil {
BeforeEach(func() {
p = setup(kv)
})
AfterEach(func() {
teardown(p)
})
}
It("Should find all keys with Find", func() {
if db, ok := p.(Find); ok {
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
key_, key, value := kv.IndexInexact(i)
@@ -38,9 +47,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
Expect(rkey).Should(Equal(key))
Expect(rvalue).Should(Equal(value), "Value for key %q (%q)", key_, key)
})
})
}
})
It("Should return error if the key is not present", func() {
It("Should return error if the key is not present", func() {
if db, ok := p.(Find); ok {
var key []byte
if kv.Len() > 0 {
key_, _ := kv.Index(kv.Len() - 1)
@@ -49,11 +60,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
rkey, _, err := db.TestFind(key)
Expect(err).Should(HaveOccurred(), "Find for key %q yield key %q", key, rkey)
Expect(err).Should(Equal(util.ErrNotFound))
})
}
}
})
if db, ok := p.(Get); ok {
It("Should only find exact key with Get", func() {
It("Should only find exact key with Get", func() {
if db, ok := p.(Get); ok {
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
key_, key, value := kv.IndexInexact(i)
@@ -69,11 +80,11 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
Expect(err).Should(Equal(util.ErrNotFound))
}
})
})
}
}
})
if db, ok := p.(NewIterator); ok {
TestIter := func(r *util.Range, _kv KeyValue) {
TestIter := func(r *util.Range, _kv KeyValue) {
if db, ok := p.(NewIterator); ok {
iter := db.TestNewIterator(r)
Expect(iter.Error()).ShouldNot(HaveOccurred())
@@ -84,45 +95,48 @@ func KeyValueTesting(rnd *rand.Rand, p DB, kv KeyValue) {
DoIteratorTesting(&t)
}
}
It("Should iterates and seeks correctly", func(done Done) {
TestIter(nil, kv.Clone())
done <- true
}, 3.0)
It("Should iterates and seeks correctly", func(done Done) {
TestIter(nil, kv.Clone())
done <- true
}, 3.0)
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
type slice struct {
r *util.Range
start, limit int
}
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
type slice struct {
r *util.Range
start, limit int
}
key_, _, _ := kv.IndexInexact(i)
for _, x := range []slice{
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
{&util.Range{Start: nil, Limit: key_}, 0, i},
} {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
TestIter(x.r, kv.Slice(x.start, x.limit))
done <- true
}, 3.0)
}
})
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
r := kv.Range(start, limit)
TestIter(&r, kv.Slice(start, limit))
key_, _, _ := kv.IndexInexact(i)
for _, x := range []slice{
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
{&util.Range{Start: nil, Limit: key_}, 0, i},
} {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
TestIter(x.r, kv.Slice(x.start, x.limit))
done <- true
}, 3.0)
})
}
}
})
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
r := kv.Range(start, limit)
TestIter(&r, kv.Slice(start, limit))
done <- true
}, 3.0)
})
}
func AllKeyValueTesting(rnd *rand.Rand, body func(kv KeyValue) DB) {
func AllKeyValueTesting(rnd *rand.Rand, body, setup func(KeyValue) DB, teardown func(DB)) {
Test := func(kv *KeyValue) func() {
return func() {
db := body(*kv)
KeyValueTesting(rnd, db, *kv)
var p DB
if body != nil {
p = body(*kv)
}
KeyValueTesting(rnd, *kv, p, setup, teardown)
}
}
@@ -57,6 +57,7 @@ const (
typeManifest = iota
typeJournal
typeTable
typeTemp
typeCount
)
@@ -91,6 +92,8 @@ func flattenType(m StorageMode, t storage.FileType) int {
return x + typeJournal
case storage.TypeTable:
return x + typeTable
case storage.TypeTemp:
return x + typeTemp
default:
panic("invalid file type")
}
@@ -107,6 +110,8 @@ func listFlattenType(m StorageMode, t storage.FileType) []int {
ret = append(ret, x+typeJournal)
case t&storage.TypeTable != 0:
ret = append(ret, x+typeTable)
case t&storage.TypeTemp != 0:
ret = append(ret, x+typeTemp)
}
}
switch {
@@ -48,6 +48,7 @@ func (t *testingDB) TestClose() {
func newTestingDB(o *opt.Options, ro *opt.ReadOptions, wo *opt.WriteOptions) *testingDB {
stor := testutil.NewStorage()
db, err := Open(stor, o)
// FIXME: This may be called from outside It, which may cause panic.
Expect(err).NotTo(HaveOccurred())
return &testingDB{
DB: db,
@@ -0,0 +1,221 @@
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package util
import (
"fmt"
"sync/atomic"
"time"
)
type buffer struct {
b []byte
miss int
}
// BufferPool is a 'buffer pool'.
type BufferPool struct {
pool [6]chan []byte
size [5]uint32
sizeMiss [5]uint32
sizeHalf [5]uint32
baseline [4]int
baselinex0 int
baselinex1 int
baseline0 int
baseline1 int
baseline2 int
close chan struct{}
get uint32
put uint32
half uint32
less uint32
equal uint32
greater uint32
miss uint32
}
func (p *BufferPool) poolNum(n int) int {
if n <= p.baseline0 && n > p.baseline0/2 {
return 0
}
for i, x := range p.baseline {
if n <= x {
return i + 1
}
}
return len(p.baseline) + 1
}
// Get returns buffer with length of n.
func (p *BufferPool) Get(n int) []byte {
if p == nil {
return make([]byte, n)
}
atomic.AddUint32(&p.get, 1)
poolNum := p.poolNum(n)
pool := p.pool[poolNum]
if poolNum == 0 {
// Fast path.
select {
case b := <-pool:
switch {
case cap(b) > n:
if cap(b)-n >= n {
atomic.AddUint32(&p.half, 1)
select {
case pool <- b:
default:
}
return make([]byte, n)
} else {
atomic.AddUint32(&p.less, 1)
return b[:n]
}
case cap(b) == n:
atomic.AddUint32(&p.equal, 1)
return b[:n]
default:
atomic.AddUint32(&p.greater, 1)
}
default:
atomic.AddUint32(&p.miss, 1)
}
return make([]byte, n, p.baseline0)
} else {
sizePtr := &p.size[poolNum-1]
select {
case b := <-pool:
switch {
case cap(b) > n:
if cap(b)-n >= n {
atomic.AddUint32(&p.half, 1)
sizeHalfPtr := &p.sizeHalf[poolNum-1]
if atomic.AddUint32(sizeHalfPtr, 1) == 20 {
atomic.StoreUint32(sizePtr, uint32(cap(b)/2))
atomic.StoreUint32(sizeHalfPtr, 0)
} else {
select {
case pool <- b:
default:
}
}
return make([]byte, n)
} else {
atomic.AddUint32(&p.less, 1)
return b[:n]
}
case cap(b) == n:
atomic.AddUint32(&p.equal, 1)
return b[:n]
default:
atomic.AddUint32(&p.greater, 1)
if uint32(cap(b)) >= atomic.LoadUint32(sizePtr) {
select {
case pool <- b:
default:
}
}
}
default:
atomic.AddUint32(&p.miss, 1)
}
if size := atomic.LoadUint32(sizePtr); uint32(n) > size {
if size == 0 {
atomic.CompareAndSwapUint32(sizePtr, 0, uint32(n))
} else {
sizeMissPtr := &p.sizeMiss[poolNum-1]
if atomic.AddUint32(sizeMissPtr, 1) == 20 {
atomic.StoreUint32(sizePtr, uint32(n))
atomic.StoreUint32(sizeMissPtr, 0)
}
}
return make([]byte, n)
} else {
return make([]byte, n, size)
}
}
}
// Put adds given buffer to the pool.
func (p *BufferPool) Put(b []byte) {
if p == nil {
return
}
atomic.AddUint32(&p.put, 1)
pool := p.pool[p.poolNum(cap(b))]
select {
case pool <- b:
default:
}
}
func (p *BufferPool) Close() {
if p == nil {
return
}
select {
case p.close <- struct{}{}:
default:
}
}
func (p *BufferPool) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("BufferPool{B·%d Z·%v Zm·%v Zh·%v G·%d P·%d H·%d <·%d =·%d >·%d M·%d}",
p.baseline0, p.size, p.sizeMiss, p.sizeHalf, p.get, p.put, p.half, p.less, p.equal, p.greater, p.miss)
}
func (p *BufferPool) drain() {
ticker := time.NewTicker(2 * time.Second)
for {
select {
case <-ticker.C:
for _, ch := range p.pool {
select {
case <-ch:
default:
}
}
case <-p.close:
for _, ch := range p.pool {
close(ch)
}
return
}
}
}
// NewBufferPool creates a new initialized 'buffer pool'.
func NewBufferPool(baseline int) *BufferPool {
if baseline <= 0 {
panic("baseline can't be <= 0")
}
p := &BufferPool{
baseline0: baseline,
baseline: [...]int{baseline / 4, baseline / 2, baseline * 2, baseline * 4},
close: make(chan struct{}, 1),
}
for i, cap := range []int{2, 2, 4, 4, 2, 1} {
p.pool[i] = make(chan []byte, cap)
}
go p.drain()
return p
}

Some files were not shown because too many files have changed in this diff Show More