mirror of
https://github.com/restic/restic.git
synced 2026-08-16 18:43:19 +00:00
Update dependencies
Among others, this updates minio-go, so that the new "eu-west-3" zone for AWS is supported.
This commit is contained in:
+445
-18
@@ -18,28 +18,36 @@ package bigtable
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/bigtable/internal/gax"
|
||||
btopt "cloud.google.com/go/bigtable/internal/option"
|
||||
"cloud.google.com/go/longrunning"
|
||||
lroauto "cloud.google.com/go/longrunning/autogen"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
durpb "github.com/golang/protobuf/ptypes/duration"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/cloudresourcemanager/v1"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
gtransport "google.golang.org/api/transport/grpc"
|
||||
btapb "google.golang.org/genproto/googleapis/bigtable/admin/v2"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
const adminAddr = "bigtableadmin.googleapis.com:443"
|
||||
|
||||
// AdminClient is a client type for performing admin operations within a specific instance.
|
||||
type AdminClient struct {
|
||||
conn *grpc.ClientConn
|
||||
tClient btapb.BigtableTableAdminClient
|
||||
conn *grpc.ClientConn
|
||||
tClient btapb.BigtableTableAdminClient
|
||||
lroClient *lroauto.OperationsClient
|
||||
|
||||
project, instance string
|
||||
|
||||
@@ -53,17 +61,32 @@ func NewAdminClient(ctx context.Context, project, instance string, opts ...optio
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Need to add scopes for long running operations (for create table & snapshots)
|
||||
o = append(o, option.WithScopes(cloudresourcemanager.CloudPlatformScope))
|
||||
o = append(o, opts...)
|
||||
conn, err := gtransport.Dial(ctx, o...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing: %v", err)
|
||||
}
|
||||
|
||||
lroClient, err := lroauto.NewOperationsClient(ctx, option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
// This error "should not happen", since we are just reusing old connection
|
||||
// and never actually need to dial.
|
||||
// If this does happen, we could leak conn. However, we cannot close conn:
|
||||
// If the user invoked the function with option.WithGRPCConn,
|
||||
// we would close a connection that's still in use.
|
||||
// TODO(pongad): investigate error conditions.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &AdminClient{
|
||||
conn: conn,
|
||||
tClient: btapb.NewBigtableTableAdminClient(conn),
|
||||
project: project,
|
||||
instance: instance,
|
||||
md: metadata.Pairs(resourcePrefixHeader, fmt.Sprintf("projects/%s/instances/%s", project, instance)),
|
||||
conn: conn,
|
||||
tClient: btapb.NewBigtableTableAdminClient(conn),
|
||||
lroClient: lroClient,
|
||||
project: project,
|
||||
instance: instance,
|
||||
md: metadata.Pairs(resourcePrefixHeader, fmt.Sprintf("projects/%s/instances/%s", project, instance)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -245,6 +268,283 @@ func (ac *AdminClient) DropRowRange(ctx context.Context, table, rowKeyPrefix str
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateTableFromSnapshot creates a table from snapshot.
|
||||
// The table will be created in the same cluster as the snapshot.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable snapshots. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (ac *AdminClient) CreateTableFromSnapshot(ctx context.Context, table, cluster, snapshot string) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, ac.md)
|
||||
prefix := ac.instancePrefix()
|
||||
snapshotPath := prefix + "/clusters/" + cluster + "/snapshots/" + snapshot
|
||||
|
||||
req := &btapb.CreateTableFromSnapshotRequest{
|
||||
Parent: prefix,
|
||||
TableId: table,
|
||||
SourceSnapshot: snapshotPath,
|
||||
}
|
||||
op, err := ac.tClient.CreateTableFromSnapshot(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp := btapb.Table{}
|
||||
return longrunning.InternalNewOperation(ac.lroClient, op).Wait(ctx, &resp)
|
||||
}
|
||||
|
||||
const DefaultSnapshotDuration time.Duration = 0
|
||||
|
||||
// Creates a new snapshot in the specified cluster from the specified source table.
|
||||
// Setting the ttl to `DefaultSnapshotDuration` will use the server side default for the duration.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable snapshots. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (ac *AdminClient) SnapshotTable(ctx context.Context, table, cluster, snapshot string, ttl time.Duration) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, ac.md)
|
||||
prefix := ac.instancePrefix()
|
||||
|
||||
var ttlProto *durpb.Duration
|
||||
|
||||
if ttl > 0 {
|
||||
ttlProto = ptypes.DurationProto(ttl)
|
||||
}
|
||||
|
||||
req := &btapb.SnapshotTableRequest{
|
||||
Name: prefix + "/tables/" + table,
|
||||
Cluster: prefix + "/clusters/" + cluster,
|
||||
SnapshotId: snapshot,
|
||||
Ttl: ttlProto,
|
||||
}
|
||||
|
||||
op, err := ac.tClient.SnapshotTable(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp := btapb.Snapshot{}
|
||||
return longrunning.InternalNewOperation(ac.lroClient, op).Wait(ctx, &resp)
|
||||
}
|
||||
|
||||
// Returns a SnapshotIterator for iterating over the snapshots in a cluster.
|
||||
// To list snapshots across all of the clusters in the instance specify "-" as the cluster.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable snapshots. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (ac *AdminClient) ListSnapshots(ctx context.Context, cluster string) *SnapshotIterator {
|
||||
ctx = mergeOutgoingMetadata(ctx, ac.md)
|
||||
prefix := ac.instancePrefix()
|
||||
clusterPath := prefix + "/clusters/" + cluster
|
||||
|
||||
it := &SnapshotIterator{}
|
||||
req := &btapb.ListSnapshotsRequest{
|
||||
Parent: clusterPath,
|
||||
}
|
||||
|
||||
fetch := func(pageSize int, pageToken string) (string, error) {
|
||||
req.PageToken = pageToken
|
||||
if pageSize > math.MaxInt32 {
|
||||
req.PageSize = math.MaxInt32
|
||||
} else {
|
||||
req.PageSize = int32(pageSize)
|
||||
}
|
||||
|
||||
resp, err := ac.tClient.ListSnapshots(ctx, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, s := range resp.Snapshots {
|
||||
snapshotInfo, err := newSnapshotInfo(s)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to parse snapshot proto %v", err)
|
||||
}
|
||||
it.items = append(it.items, snapshotInfo)
|
||||
}
|
||||
return resp.NextPageToken, nil
|
||||
}
|
||||
bufLen := func() int { return len(it.items) }
|
||||
takeBuf := func() interface{} { b := it.items; it.items = nil; return b }
|
||||
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, bufLen, takeBuf)
|
||||
|
||||
return it
|
||||
}
|
||||
|
||||
func newSnapshotInfo(snapshot *btapb.Snapshot) (*SnapshotInfo, error) {
|
||||
nameParts := strings.Split(snapshot.Name, "/")
|
||||
name := nameParts[len(nameParts)-1]
|
||||
tablePathParts := strings.Split(snapshot.SourceTable.Name, "/")
|
||||
tableId := tablePathParts[len(tablePathParts)-1]
|
||||
|
||||
createTime, err := ptypes.Timestamp(snapshot.CreateTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid createTime: %v", err)
|
||||
}
|
||||
|
||||
deleteTime, err := ptypes.Timestamp(snapshot.DeleteTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid deleteTime: %v", err)
|
||||
}
|
||||
|
||||
return &SnapshotInfo{
|
||||
Name: name,
|
||||
SourceTable: tableId,
|
||||
DataSize: snapshot.DataSizeBytes,
|
||||
CreateTime: createTime,
|
||||
DeleteTime: deleteTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// An EntryIterator iterates over log entries.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable snapshots. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
type SnapshotIterator struct {
|
||||
items []*SnapshotInfo
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See https://godoc.org/google.golang.org/api/iterator package for details.
|
||||
func (it *SnapshotIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done
|
||||
// (https://godoc.org/google.golang.org/api/iterator) if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *SnapshotIterator) Next() (*SnapshotInfo, error) {
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item := it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
type SnapshotInfo struct {
|
||||
Name string
|
||||
SourceTable string
|
||||
DataSize int64
|
||||
CreateTime time.Time
|
||||
DeleteTime time.Time
|
||||
}
|
||||
|
||||
// Get snapshot metadata.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable snapshots. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (ac *AdminClient) SnapshotInfo(ctx context.Context, cluster, snapshot string) (*SnapshotInfo, error) {
|
||||
ctx = mergeOutgoingMetadata(ctx, ac.md)
|
||||
prefix := ac.instancePrefix()
|
||||
clusterPath := prefix + "/clusters/" + cluster
|
||||
snapshotPath := clusterPath + "/snapshots/" + snapshot
|
||||
|
||||
req := &btapb.GetSnapshotRequest{
|
||||
Name: snapshotPath,
|
||||
}
|
||||
|
||||
resp, err := ac.tClient.GetSnapshot(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newSnapshotInfo(resp)
|
||||
}
|
||||
|
||||
// Delete a snapshot in a cluster.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable snapshots. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (ac *AdminClient) DeleteSnapshot(ctx context.Context, cluster, snapshot string) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, ac.md)
|
||||
prefix := ac.instancePrefix()
|
||||
clusterPath := prefix + "/clusters/" + cluster
|
||||
snapshotPath := clusterPath + "/snapshots/" + snapshot
|
||||
|
||||
req := &btapb.DeleteSnapshotRequest{
|
||||
Name: snapshotPath,
|
||||
}
|
||||
_, err := ac.tClient.DeleteSnapshot(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// getConsistencyToken gets the consistency token for a table.
|
||||
func (ac *AdminClient) getConsistencyToken(ctx context.Context, tableName string) (string, error) {
|
||||
req := &btapb.GenerateConsistencyTokenRequest{
|
||||
Name: tableName,
|
||||
}
|
||||
resp, err := ac.tClient.GenerateConsistencyToken(ctx, req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetConsistencyToken(), nil
|
||||
}
|
||||
|
||||
// isConsistent checks if a token is consistent for a table.
|
||||
func (ac *AdminClient) isConsistent(ctx context.Context, tableName, token string) (bool, error) {
|
||||
req := &btapb.CheckConsistencyRequest{
|
||||
Name: tableName,
|
||||
ConsistencyToken: token,
|
||||
}
|
||||
var resp *btapb.CheckConsistencyResponse
|
||||
|
||||
// Retry calls on retryable errors to avoid losing the token gathered before.
|
||||
err := gax.Invoke(ctx, func(ctx context.Context) error {
|
||||
var err error
|
||||
resp, err = ac.tClient.CheckConsistency(ctx, req)
|
||||
return err
|
||||
}, retryOptions...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return resp.GetConsistent(), nil
|
||||
}
|
||||
|
||||
// WaitForReplication waits until all the writes committed before the call started have been propagated to all the clusters in the instance via replication.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable replication. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (ac *AdminClient) WaitForReplication(ctx context.Context, table string) error {
|
||||
// Get the token.
|
||||
prefix := ac.instancePrefix()
|
||||
tableName := prefix + "/tables/" + table
|
||||
token, err := ac.getConsistencyToken(ctx, tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Periodically check if the token is consistent.
|
||||
timer := time.NewTicker(time.Second * 10)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
consistent, err := ac.isConsistent(ctx, tableName, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if consistent {
|
||||
return nil
|
||||
}
|
||||
// Sleep for a bit or until the ctx is cancelled.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const instanceAdminAddr = "bigtableadmin.googleapis.com:443"
|
||||
|
||||
// InstanceAdminClient is a client type for performing admin operations on instances.
|
||||
@@ -336,24 +636,55 @@ type InstanceConf struct {
|
||||
InstanceType InstanceType
|
||||
}
|
||||
|
||||
// InstanceWithClustersConfig contains the information necessary to create an Instance
|
||||
type InstanceWithClustersConfig struct {
|
||||
InstanceID, DisplayName string
|
||||
Clusters []ClusterConfig
|
||||
InstanceType InstanceType
|
||||
}
|
||||
|
||||
var instanceNameRegexp = regexp.MustCompile(`^projects/([^/]+)/instances/([a-z][-a-z0-9]*)$`)
|
||||
|
||||
// CreateInstance creates a new instance in the project.
|
||||
// This method will return when the instance has been created or when an error occurs.
|
||||
func (iac *InstanceAdminClient) CreateInstance(ctx context.Context, conf *InstanceConf) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, iac.md)
|
||||
req := &btapb.CreateInstanceRequest{
|
||||
Parent: "projects/" + iac.project,
|
||||
InstanceId: conf.InstanceId,
|
||||
Instance: &btapb.Instance{DisplayName: conf.DisplayName, Type: btapb.Instance_Type(conf.InstanceType)},
|
||||
Clusters: map[string]*btapb.Cluster{
|
||||
conf.ClusterId: {
|
||||
ServeNodes: conf.NumNodes,
|
||||
DefaultStorageType: conf.StorageType.proto(),
|
||||
Location: "projects/" + iac.project + "/locations/" + conf.Zone,
|
||||
newConfig := InstanceWithClustersConfig{
|
||||
InstanceID: conf.InstanceId,
|
||||
DisplayName: conf.DisplayName,
|
||||
InstanceType: conf.InstanceType,
|
||||
Clusters: []ClusterConfig{
|
||||
{
|
||||
InstanceID: conf.InstanceId,
|
||||
ClusterID: conf.ClusterId,
|
||||
Zone: conf.Zone,
|
||||
NumNodes: conf.NumNodes,
|
||||
StorageType: conf.StorageType,
|
||||
},
|
||||
},
|
||||
}
|
||||
return iac.CreateInstanceWithClusters(ctx, &newConfig)
|
||||
}
|
||||
|
||||
// CreateInstance creates a new instance with configured clusters in the project.
|
||||
// This method will return when the instance has been created or when an error occurs.
|
||||
//
|
||||
// Instances with multiple clusters are part of a private alpha release of Cloud Bigtable replication.
|
||||
// This feature is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (iac *InstanceAdminClient) CreateInstanceWithClusters(ctx context.Context, conf *InstanceWithClustersConfig) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, iac.md)
|
||||
clusters := make(map[string]*btapb.Cluster)
|
||||
for _, cluster := range conf.Clusters {
|
||||
clusters[cluster.ClusterID] = cluster.proto(iac.project)
|
||||
}
|
||||
|
||||
req := &btapb.CreateInstanceRequest{
|
||||
Parent: "projects/" + iac.project,
|
||||
InstanceId: conf.InstanceID,
|
||||
Instance: &btapb.Instance{DisplayName: conf.DisplayName, Type: btapb.Instance_Type(conf.InstanceType)},
|
||||
Clusters: clusters,
|
||||
}
|
||||
|
||||
lro, err := iac.iClient.CreateInstance(ctx, req)
|
||||
if err != nil {
|
||||
@@ -421,3 +752,99 @@ func (iac *InstanceAdminClient) InstanceInfo(ctx context.Context, instanceId str
|
||||
DisplayName: res.DisplayName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClusterConfig contains the information necessary to create a cluster
|
||||
type ClusterConfig struct {
|
||||
InstanceID, ClusterID, Zone string
|
||||
NumNodes int32
|
||||
StorageType StorageType
|
||||
}
|
||||
|
||||
func (cc *ClusterConfig) proto(project string) *btapb.Cluster {
|
||||
return &btapb.Cluster{
|
||||
ServeNodes: cc.NumNodes,
|
||||
DefaultStorageType: cc.StorageType.proto(),
|
||||
Location: "projects/" + project + "/locations/" + cc.Zone,
|
||||
}
|
||||
}
|
||||
|
||||
// ClusterInfo represents information about a cluster.
|
||||
type ClusterInfo struct {
|
||||
Name string // name of the cluster
|
||||
Zone string // GCP zone of the cluster (e.g. "us-central1-a")
|
||||
ServeNodes int // number of allocated serve nodes
|
||||
State string // state of the cluster
|
||||
}
|
||||
|
||||
// CreateCluster creates a new cluster in an instance.
|
||||
// This method will return when the cluster has been created or when an error occurs.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable replication. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (iac *InstanceAdminClient) CreateCluster(ctx context.Context, conf *ClusterConfig) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, iac.md)
|
||||
|
||||
req := &btapb.CreateClusterRequest{
|
||||
Parent: "projects/" + iac.project + "/instances/" + conf.InstanceID,
|
||||
ClusterId: conf.ClusterID,
|
||||
Cluster: conf.proto(iac.project),
|
||||
}
|
||||
|
||||
lro, err := iac.iClient.CreateCluster(ctx, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp := btapb.Cluster{}
|
||||
return longrunning.InternalNewOperation(iac.lroClient, lro).Wait(ctx, &resp)
|
||||
}
|
||||
|
||||
// DeleteCluster deletes a cluster from an instance.
|
||||
//
|
||||
// This is a private alpha release of Cloud Bigtable replication. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (iac *InstanceAdminClient) DeleteCluster(ctx context.Context, instanceId, clusterId string) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, iac.md)
|
||||
req := &btapb.DeleteClusterRequest{"projects/" + iac.project + "/instances/" + instanceId + "/clusters/" + clusterId}
|
||||
_, err := iac.iClient.DeleteCluster(ctx, req)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateCluster updates attributes of a cluster
|
||||
func (iac *InstanceAdminClient) UpdateCluster(ctx context.Context, instanceId, clusterId string, serveNodes int32) error {
|
||||
ctx = mergeOutgoingMetadata(ctx, iac.md)
|
||||
cluster := &btapb.Cluster{
|
||||
Name: "projects/" + iac.project + "/instances/" + instanceId + "/clusters/" + clusterId,
|
||||
ServeNodes: serveNodes}
|
||||
lro, err := iac.iClient.UpdateCluster(ctx, cluster)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return longrunning.InternalNewOperation(iac.lroClient, lro).Wait(ctx, nil)
|
||||
}
|
||||
|
||||
// Clusters lists the clusters in an instance.
|
||||
func (iac *InstanceAdminClient) Clusters(ctx context.Context, instanceId string) ([]*ClusterInfo, error) {
|
||||
ctx = mergeOutgoingMetadata(ctx, iac.md)
|
||||
req := &btapb.ListClustersRequest{Parent: "projects/" + iac.project + "/instances/" + instanceId}
|
||||
res, err := iac.iClient.ListClusters(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO(garyelliott): Deal with failed_locations.
|
||||
var cis []*ClusterInfo
|
||||
for _, c := range res.Clusters {
|
||||
nameParts := strings.Split(c.Name, "/")
|
||||
locParts := strings.Split(c.Location, "/")
|
||||
cis = append(cis, &ClusterInfo{
|
||||
Name: nameParts[len(nameParts)-1],
|
||||
Zone: locParts[len(locParts)-1],
|
||||
ServeNodes: int(c.ServeNodes),
|
||||
State: c.State.String(),
|
||||
})
|
||||
}
|
||||
return cis, nil
|
||||
}
|
||||
|
||||
+125
-2
@@ -15,13 +15,16 @@
|
||||
package bigtable
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
|
||||
"fmt"
|
||||
"golang.org/x/net/context"
|
||||
"reflect"
|
||||
"google.golang.org/api/iterator"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -97,6 +100,9 @@ func TestAdminIntegration(t *testing.T) {
|
||||
if got, want := list(), []string{"myothertable", "mytable"}; !containsAll(got, want) {
|
||||
t.Errorf("adminClient.Tables returned %#v, want %#v", got, want)
|
||||
}
|
||||
|
||||
adminClient.WaitForReplication(ctx, "mytable")
|
||||
|
||||
if err := adminClient.DeleteTable(ctx, "myothertable"); err != nil {
|
||||
t.Fatalf("Deleting table: %v", err)
|
||||
}
|
||||
@@ -126,7 +132,7 @@ func TestAdminIntegration(t *testing.T) {
|
||||
}
|
||||
sort.Strings(tblInfo.Families)
|
||||
wantFams := []string{"fam1", "fam2"}
|
||||
if !reflect.DeepEqual(tblInfo.Families, wantFams) {
|
||||
if !testutil.Equal(tblInfo.Families, wantFams) {
|
||||
t.Errorf("Column family mismatch, got %v, want %v", tblInfo.Families, wantFams)
|
||||
}
|
||||
|
||||
@@ -176,3 +182,120 @@ func TestAdminIntegration(t *testing.T) {
|
||||
t.Errorf("Invalid row count after dropping range: got %v, want %v", gotRowCount, 5)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminSnapshotIntegration(t *testing.T) {
|
||||
testEnv, err := NewIntegrationEnv()
|
||||
if err != nil {
|
||||
t.Fatalf("IntegrationEnv: %v", err)
|
||||
}
|
||||
defer testEnv.Close()
|
||||
|
||||
if !testEnv.Config().UseProd {
|
||||
t.Skip("emulator doesn't support snapshots")
|
||||
}
|
||||
|
||||
timeout := 2 * time.Second
|
||||
if testEnv.Config().UseProd {
|
||||
timeout = 5 * time.Minute
|
||||
}
|
||||
ctx, _ := context.WithTimeout(context.Background(), timeout)
|
||||
|
||||
adminClient, err := testEnv.NewAdminClient()
|
||||
if err != nil {
|
||||
t.Fatalf("NewAdminClient: %v", err)
|
||||
}
|
||||
defer adminClient.Close()
|
||||
|
||||
table := testEnv.Config().Table
|
||||
cluster := testEnv.Config().Cluster
|
||||
|
||||
list := func(cluster string) ([]*SnapshotInfo, error) {
|
||||
infos := []*SnapshotInfo(nil)
|
||||
|
||||
it := adminClient.ListSnapshots(ctx, cluster)
|
||||
for {
|
||||
s, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
infos = append(infos, s)
|
||||
}
|
||||
return infos, err
|
||||
}
|
||||
|
||||
// Delete the table at the end of the test. Schedule ahead of time
|
||||
// in case the client fails
|
||||
defer adminClient.DeleteTable(ctx, table)
|
||||
|
||||
if err := adminClient.CreateTable(ctx, table); err != nil {
|
||||
t.Fatalf("Creating table: %v", err)
|
||||
}
|
||||
|
||||
// Precondition: no snapshots
|
||||
snapshots, err := list(cluster)
|
||||
if err != nil {
|
||||
t.Fatalf("Initial snapshot list: %v", err)
|
||||
}
|
||||
if got, want := len(snapshots), 0; got != want {
|
||||
t.Fatalf("Initial snapshot list len: %d, want: %d", got, want)
|
||||
}
|
||||
|
||||
// Create snapshot
|
||||
defer adminClient.DeleteSnapshot(ctx, cluster, "mysnapshot")
|
||||
|
||||
if err = adminClient.SnapshotTable(ctx, table, cluster, "mysnapshot", 5*time.Hour); err != nil {
|
||||
t.Fatalf("Creating snaphot: %v", err)
|
||||
}
|
||||
|
||||
// List snapshot
|
||||
snapshots, err = list(cluster)
|
||||
if err != nil {
|
||||
t.Fatalf("Listing snapshots: %v", err)
|
||||
}
|
||||
if got, want := len(snapshots), 1; got != want {
|
||||
t.Fatalf("Listing snapshot count: %d, want: %d", got, want)
|
||||
}
|
||||
if got, want := snapshots[0].Name, "mysnapshot"; got != want {
|
||||
t.Fatalf("Snapshot name: %s, want: %s", got, want)
|
||||
}
|
||||
if got, want := snapshots[0].SourceTable, table; got != want {
|
||||
t.Fatalf("Snapshot SourceTable: %s, want: %s", got, want)
|
||||
}
|
||||
if got, want := snapshots[0].DeleteTime, snapshots[0].CreateTime.Add(5*time.Hour); math.Abs(got.Sub(want).Minutes()) > 1 {
|
||||
t.Fatalf("Snapshot DeleteTime: %s, want: %s", got, want)
|
||||
}
|
||||
|
||||
// Get snapshot
|
||||
snapshot, err := adminClient.SnapshotInfo(ctx, cluster, "mysnapshot")
|
||||
if err != nil {
|
||||
t.Fatalf("SnapshotInfo: %v", snapshot)
|
||||
}
|
||||
if got, want := *snapshot, *snapshots[0]; got != want {
|
||||
t.Fatalf("SnapshotInfo: %v, want: %v", got, want)
|
||||
}
|
||||
|
||||
// Restore
|
||||
restoredTable := table + "-restored"
|
||||
defer adminClient.DeleteTable(ctx, restoredTable)
|
||||
if err = adminClient.CreateTableFromSnapshot(ctx, restoredTable, cluster, "mysnapshot"); err != nil {
|
||||
t.Fatalf("CreateTableFromSnapshot: %v", err)
|
||||
}
|
||||
if _, err := adminClient.TableInfo(ctx, restoredTable); err != nil {
|
||||
t.Fatalf("Restored TableInfo: %v", err)
|
||||
}
|
||||
|
||||
// Delete snapshot
|
||||
if err = adminClient.DeleteSnapshot(ctx, cluster, "mysnapshot"); err != nil {
|
||||
t.Fatalf("DeleteSnapshot: %v", err)
|
||||
}
|
||||
snapshots, err = list(cluster)
|
||||
if err != nil {
|
||||
t.Fatalf("List after Delete: %v", err)
|
||||
}
|
||||
if got, want := len(snapshots), 0; got != want {
|
||||
t.Fatalf("List after delete len: %d, want: %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
+46
-11
@@ -44,10 +44,28 @@ type Client struct {
|
||||
conn *grpc.ClientConn
|
||||
client btpb.BigtableClient
|
||||
project, instance string
|
||||
// App Profiles are part of the private alpha release of Cloud Bigtable replication.
|
||||
// This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
appProfile string
|
||||
}
|
||||
|
||||
// ClientConfig has configurations for the client.
|
||||
type ClientConfig struct {
|
||||
// The id of the app profile to associate with all data operations sent from this client.
|
||||
// If unspecified, the default app profile for the instance will be used.
|
||||
AppProfile string
|
||||
}
|
||||
|
||||
// NewClient creates a new Client for a given project and instance.
|
||||
// The default ClientConfig will be used.
|
||||
func NewClient(ctx context.Context, project, instance string, opts ...option.ClientOption) (*Client, error) {
|
||||
return NewClientWithConfig(ctx, project, instance, ClientConfig{}, opts...)
|
||||
}
|
||||
|
||||
func NewClientWithConfig(ctx context.Context, project, instance string, config ClientConfig, opts ...option.ClientOption) (*Client, error) {
|
||||
o, err := btopt.DefaultClientOptions(prodAddr, Scope, clientUserAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -130,9 +148,16 @@ func (t *Table) ReadRows(ctx context.Context, arg RowSet, f func(Row) bool, opts
|
||||
|
||||
var prevRowKey string
|
||||
err := gax.Invoke(ctx, func(ctx context.Context) error {
|
||||
if !arg.valid() {
|
||||
// Empty row set, no need to make an API call.
|
||||
// NOTE: we must return early if arg == RowList{} because reading
|
||||
// an empty RowList from bigtable returns all rows from that table.
|
||||
return nil
|
||||
}
|
||||
req := &btpb.ReadRowsRequest{
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
Rows: arg.proto(),
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
AppProfileId: t.c.appProfile,
|
||||
Rows: arg.proto(),
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt.set(req)
|
||||
@@ -313,7 +338,7 @@ func (r RowRange) retainRowsAfter(lastRowKey string) RowSet {
|
||||
}
|
||||
|
||||
func (r RowRange) valid() bool {
|
||||
return r.start < r.limit
|
||||
return r.Unbounded() || r.start < r.limit
|
||||
}
|
||||
|
||||
// RowRangeList is a sequence of RowRanges representing the union of the ranges.
|
||||
@@ -440,9 +465,10 @@ func (t *Table) Apply(ctx context.Context, row string, m *Mutation, opts ...Appl
|
||||
var callOptions []gax.CallOption
|
||||
if m.cond == nil {
|
||||
req := &btpb.MutateRowRequest{
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
RowKey: []byte(row),
|
||||
Mutations: m.ops,
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
AppProfileId: t.c.appProfile,
|
||||
RowKey: []byte(row),
|
||||
Mutations: m.ops,
|
||||
}
|
||||
if mutationsAreRetryable(m.ops) {
|
||||
callOptions = retryOptions
|
||||
@@ -461,13 +487,20 @@ func (t *Table) Apply(ctx context.Context, row string, m *Mutation, opts ...Appl
|
||||
|
||||
req := &btpb.CheckAndMutateRowRequest{
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
AppProfileId: t.c.appProfile,
|
||||
RowKey: []byte(row),
|
||||
PredicateFilter: m.cond.proto(),
|
||||
}
|
||||
if m.mtrue != nil {
|
||||
if m.mtrue.cond != nil {
|
||||
return errors.New("bigtable: conditional mutations cannot be nested")
|
||||
}
|
||||
req.TrueMutations = m.mtrue.ops
|
||||
}
|
||||
if m.mfalse != nil {
|
||||
if m.mfalse.cond != nil {
|
||||
return errors.New("bigtable: conditional mutations cannot be nested")
|
||||
}
|
||||
req.FalseMutations = m.mfalse.ops
|
||||
}
|
||||
if mutationsAreRetryable(req.TrueMutations) && mutationsAreRetryable(req.FalseMutations) {
|
||||
@@ -670,8 +703,9 @@ func (t *Table) doApplyBulk(ctx context.Context, entryErrs []*entryErr, opts ...
|
||||
entries[i] = entryErr.Entry
|
||||
}
|
||||
req := &btpb.MutateRowsRequest{
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
Entries: entries,
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
AppProfileId: t.c.appProfile,
|
||||
Entries: entries,
|
||||
}
|
||||
stream, err := t.c.client.MutateRows(ctx, req)
|
||||
if err != nil {
|
||||
@@ -729,9 +763,10 @@ func (ts Timestamp) TruncateToMilliseconds() Timestamp {
|
||||
func (t *Table) ApplyReadModifyWrite(ctx context.Context, row string, m *ReadModifyWrite) (Row, error) {
|
||||
ctx = mergeOutgoingMetadata(ctx, t.md)
|
||||
req := &btpb.ReadModifyWriteRowRequest{
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
RowKey: []byte(row),
|
||||
Rules: m.ops,
|
||||
TableName: t.c.fullTableName(t.table),
|
||||
AppProfileId: t.c.appProfile,
|
||||
RowKey: []byte(row),
|
||||
Rules: m.ops,
|
||||
}
|
||||
res, err := t.c.client.ReadModifyWriteRow(ctx, req)
|
||||
if err != nil {
|
||||
|
||||
+162
-22
@@ -19,13 +19,16 @@ package bigtable
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestPrefix(t *testing.T) {
|
||||
@@ -53,6 +56,29 @@ func TestPrefix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyErrors(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
table := &Table{
|
||||
c: &Client{
|
||||
project: "P",
|
||||
instance: "I",
|
||||
},
|
||||
table: "t",
|
||||
}
|
||||
f := ColumnFilter("C")
|
||||
m := NewMutation()
|
||||
m.DeleteRow()
|
||||
// Test nested conditional mutations.
|
||||
cm := NewCondMutation(f, NewCondMutation(f, m, nil), nil)
|
||||
if err := table.Apply(ctx, "x", cm); err == nil {
|
||||
t.Error("got nil, want error")
|
||||
}
|
||||
cm = NewCondMutation(f, nil, NewCondMutation(f, m, nil))
|
||||
if err := table.Apply(ctx, "x", cm); err == nil {
|
||||
t.Error("got nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientIntegration(t *testing.T) {
|
||||
start := time.Now()
|
||||
lastCheckpoint := start
|
||||
@@ -67,14 +93,16 @@ func TestClientIntegration(t *testing.T) {
|
||||
t.Fatalf("IntegrationEnv: %v", err)
|
||||
}
|
||||
|
||||
timeout := 30 * time.Second
|
||||
var timeout time.Duration
|
||||
if testEnv.Config().UseProd {
|
||||
timeout = 5 * time.Minute
|
||||
timeout = 10 * time.Minute
|
||||
t.Logf("Running test against production")
|
||||
} else {
|
||||
timeout = 1 * time.Minute
|
||||
t.Logf("bttest.Server running on %s", testEnv.Config().AdminEndpoint)
|
||||
}
|
||||
ctx, _ := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
client, err := testEnv.NewClient()
|
||||
if err != nil {
|
||||
@@ -126,6 +154,11 @@ func TestClientIntegration(t *testing.T) {
|
||||
}
|
||||
checkpoint("inserted initial data")
|
||||
|
||||
if err := adminClient.WaitForReplication(ctx, table); err != nil {
|
||||
t.Errorf("Waiting for replication for table %q: %v", table, err)
|
||||
}
|
||||
checkpoint("waited for replication")
|
||||
|
||||
// Do a conditional mutation with a complex filter.
|
||||
mutTrue := NewMutation()
|
||||
mutTrue.Set("follows", "wmckinley", 0, []byte("1"))
|
||||
@@ -156,7 +189,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "jadams", Column: "follows:tjefferson", Value: []byte("1")},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(row, wantRow) {
|
||||
if !testutil.Equal(row, wantRow) {
|
||||
t.Errorf("Read row mismatch.\n got %#v\nwant %#v", row, wantRow)
|
||||
}
|
||||
checkpoint("tested ReadRow")
|
||||
@@ -319,6 +352,12 @@ func TestClientIntegration(t *testing.T) {
|
||||
filter: ConditionFilter(ChainFilters(ColumnFilter(".*j.*"), ColumnFilter(".*mckinley.*")), StripValueFilter(), nil),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
desc: "chain that ends with an interleave that has no match. covers #804",
|
||||
rr: RowRange{},
|
||||
filter: ConditionFilter(ChainFilters(ColumnFilter(".*j.*"), InterleaveFilters(ColumnFilter(".*x.*"), ColumnFilter(".*z.*"))), StripValueFilter(), nil),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tc := range readTests {
|
||||
var opts []ReadOption
|
||||
@@ -442,9 +481,13 @@ func TestClientIntegration(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ApplyReadModifyWrite %+v: %v", step.rmw, err)
|
||||
}
|
||||
// Make sure the modified cell returned by the RMW operation has a timestamp.
|
||||
if row["counter"][0].Timestamp == 0 {
|
||||
t.Errorf("RMW returned cell timestamp: got %v, want > 0", row["counter"][0].Timestamp)
|
||||
}
|
||||
clearTimestamps(row)
|
||||
wantRow := Row{"counter": []ReadItem{{Row: "gwashington", Column: "counter:likes", Value: step.want}}}
|
||||
if !reflect.DeepEqual(row, wantRow) {
|
||||
if !testutil.Equal(row, wantRow) {
|
||||
t.Fatalf("After %s,\n got %v\nwant %v", step.desc, row, wantRow)
|
||||
}
|
||||
}
|
||||
@@ -498,7 +541,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 1000, Value: []byte("val-1")},
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 0, Value: []byte("val-0")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell with multiple versions,\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
// Do the same read, but filter to the latest two versions.
|
||||
@@ -512,7 +555,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 3000, Value: []byte("val-3")},
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 2000, Value: []byte("val-2")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell with multiple versions and LatestNFilter(2),\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
// Check cell offset / limit
|
||||
@@ -525,7 +568,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "testrow", Column: "ts:col", Timestamp: 2000, Value: []byte("val-2")},
|
||||
{Row: "testrow", Column: "ts:col", Timestamp: 1000, Value: []byte("val-1")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell with multiple versions and CellsPerRowLimitFilter(3),\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
r, err = tbl.ReadRow(ctx, "testrow", RowFilter(CellsPerRowOffsetFilter(3)))
|
||||
@@ -539,7 +582,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 1000, Value: []byte("val-1")},
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 0, Value: []byte("val-0")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell with multiple versions and CellsPerRowOffsetFilter(3),\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
// Check timestamp range filtering (with truncation)
|
||||
@@ -553,7 +596,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 2000, Value: []byte("val-2")},
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 1000, Value: []byte("val-1")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell with multiple versions and TimestampRangeFilter(1000, 3000),\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
r, err = tbl.ReadRow(ctx, "testrow", RowFilter(TimestampRangeFilterMicros(1000, 0)))
|
||||
@@ -568,7 +611,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 2000, Value: []byte("val-2")},
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 1000, Value: []byte("val-1")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell with multiple versions and TimestampRangeFilter(1000, 0),\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
// Delete non-existing cells, no such column family in this row
|
||||
@@ -585,7 +628,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Reading row: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell was deleted unexpectly,\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
// Delete non-existing cells, no such column in this column family
|
||||
@@ -599,7 +642,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Reading row: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell was deleted unexpectly,\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
// Delete the cell with timestamp 2000 and repeat the last read,
|
||||
@@ -619,7 +662,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 3000, Value: []byte("val-3")},
|
||||
{Row: "testrow", Column: "ts:col2", Timestamp: 2000, Value: []byte("val-2")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Cell with multiple versions and LatestNFilter(2), after deleting timestamp 2000,\n got %v\nwant %v", r, wantRow)
|
||||
}
|
||||
checkpoint("tested multiple versions in a cell")
|
||||
@@ -654,7 +697,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
wantRow = Row{"ts": []ReadItem{
|
||||
{Row: "row1", Column: "ts:col", Timestamp: 0, Value: []byte("3")},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("column family was not deleted.\n got %v\n want %v", r, wantRow)
|
||||
}
|
||||
|
||||
@@ -672,7 +715,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "row2", Column: "status:start", Timestamp: 0, Value: []byte("1")},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Column family was deleted unexpectly.\n got %v\n want %v", r, wantRow)
|
||||
}
|
||||
checkpoint("tested family delete")
|
||||
@@ -700,7 +743,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "row3", Column: "status:start", Timestamp: 0, Value: []byte("1")},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Column was not deleted.\n got %v\n want %v", r, wantRow)
|
||||
}
|
||||
mut = NewMutation()
|
||||
@@ -717,7 +760,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
{Row: "row3", Column: "status:end", Timestamp: 0, Value: []byte("3")},
|
||||
},
|
||||
}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Column was not deleted.\n got %v\n want %v", r, wantRow)
|
||||
}
|
||||
mut = NewMutation()
|
||||
@@ -742,7 +785,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Reading row: %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Column was not deleted correctly.\n got %v\n want %v", r, wantRow)
|
||||
}
|
||||
checkpoint("tested column delete")
|
||||
@@ -791,7 +834,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
wantRow = Row{"ts": []ReadItem{
|
||||
{Row: "bigrow", Column: "ts:col", Value: bigBytes},
|
||||
}}
|
||||
if !reflect.DeepEqual(r, wantRow) {
|
||||
if !testutil.Equal(r, wantRow) {
|
||||
t.Errorf("Big read returned incorrect bytes: %v", r)
|
||||
}
|
||||
// Now write 1000 rows, each with 82 KB values, then scan them all.
|
||||
@@ -879,7 +922,7 @@ func TestClientIntegration(t *testing.T) {
|
||||
wantItems = append(wantItems, ReadItem{Row: rowKey, Column: "bulk:" + val, Value: []byte("1")})
|
||||
}
|
||||
wantRow := Row{"bulk": wantItems}
|
||||
if !reflect.DeepEqual(row, wantRow) {
|
||||
if !testutil.Equal(row, wantRow) {
|
||||
t.Errorf("Read row mismatch.\n got %#v\nwant %#v", row, wantRow)
|
||||
}
|
||||
}
|
||||
@@ -902,6 +945,103 @@ func TestClientIntegration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type requestCountingInterceptor struct {
|
||||
grpc.ClientStream
|
||||
requestCallback func()
|
||||
}
|
||||
|
||||
func (i *requestCountingInterceptor) SendMsg(m interface{}) error {
|
||||
i.requestCallback()
|
||||
return i.ClientStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (i *requestCountingInterceptor) RecvMsg(m interface{}) error {
|
||||
return i.ClientStream.RecvMsg(m)
|
||||
}
|
||||
|
||||
func requestCallback(callback func()) func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
clientStream, err := streamer(ctx, desc, cc, method, opts...)
|
||||
return &requestCountingInterceptor{
|
||||
ClientStream: clientStream,
|
||||
requestCallback: callback,
|
||||
}, err
|
||||
}
|
||||
}
|
||||
|
||||
// TestReadRowsInvalidRowSet verifies that the client doesn't send ReadRows() requests with invalid RowSets.
|
||||
func TestReadRowsInvalidRowSet(t *testing.T) {
|
||||
testEnv, err := NewEmulatedEnv(IntegrationTestConfig{})
|
||||
if err != nil {
|
||||
t.Fatalf("NewEmulatedEnv failed: %v", err)
|
||||
}
|
||||
var requestCount int
|
||||
incrementRequestCount := func() { requestCount++ }
|
||||
conn, err := grpc.Dial(testEnv.server.Addr, grpc.WithInsecure(), grpc.WithBlock(),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(100<<20), grpc.MaxCallRecvMsgSize(100<<20)),
|
||||
grpc.WithStreamInterceptor(requestCallback(incrementRequestCount)),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.Dial failed: %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
adminClient, err := NewAdminClient(ctx, testEnv.config.Project, testEnv.config.Instance, option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient failed: %v", err)
|
||||
}
|
||||
defer adminClient.Close()
|
||||
if err := adminClient.CreateTable(ctx, testEnv.config.Table); err != nil {
|
||||
t.Fatalf("CreateTable(%v) failed: %v", testEnv.config.Table, err)
|
||||
}
|
||||
client, err := NewClient(ctx, testEnv.config.Project, testEnv.config.Instance, option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient failed: %v", err)
|
||||
}
|
||||
defer client.Close()
|
||||
table := client.Open(testEnv.config.Table)
|
||||
tests := []struct {
|
||||
rr RowSet
|
||||
valid bool
|
||||
}{
|
||||
{
|
||||
rr: RowRange{},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
rr: RowRange{start: "b"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
rr: RowRange{start: "b", limit: "c"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
rr: RowRange{start: "b", limit: "a"},
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
rr: RowList{"a"},
|
||||
valid: true,
|
||||
},
|
||||
{
|
||||
rr: RowList{},
|
||||
valid: false,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
requestCount = 0
|
||||
err = table.ReadRows(ctx, test.rr, func(r Row) bool { return true })
|
||||
if err != nil {
|
||||
t.Fatalf("ReadRows(%v) failed: %v", test.rr, err)
|
||||
}
|
||||
requestValid := requestCount != 0
|
||||
if requestValid != test.valid {
|
||||
t.Errorf("%s: got %v, want %v", test.rr, requestValid, test.valid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func formatReadItem(ri ReadItem) string {
|
||||
// Use the column qualifier only to make the test data briefer.
|
||||
col := ri.Column[strings.Index(ri.Column, ":")+1:]
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ import (
|
||||
|
||||
func ExampleNewServer() {
|
||||
|
||||
srv, err := bttest.NewServer("127.0.0.1:0")
|
||||
srv, err := bttest.NewServer("localhost:0")
|
||||
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
|
||||
+136
-97
@@ -19,7 +19,7 @@ Package bttest contains test helpers for working with the bigtable package.
|
||||
|
||||
To use a Server, create it, and then connect to it with no security:
|
||||
(The project/instance values are ignored.)
|
||||
srv, err := bttest.NewServer("127.0.0.1:0")
|
||||
srv, err := bttest.NewServer("localhost:0")
|
||||
...
|
||||
conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
...
|
||||
@@ -45,12 +45,14 @@ import (
|
||||
|
||||
emptypb "github.com/golang/protobuf/ptypes/empty"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
"github.com/google/btree"
|
||||
"golang.org/x/net/context"
|
||||
btapb "google.golang.org/genproto/googleapis/bigtable/admin/v2"
|
||||
btpb "google.golang.org/genproto/googleapis/bigtable/v2"
|
||||
statpb "google.golang.org/genproto/googleapis/rpc/status"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// Server is an in-memory Cloud Bigtable fake.
|
||||
@@ -162,7 +164,7 @@ func (s *server) DeleteTable(ctx context.Context, req *btapb.DeleteTableRequest)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if _, ok := s.tables[req.Name]; !ok {
|
||||
return nil, grpc.Errorf(codes.NotFound, "table %q not found", req.Name)
|
||||
return nil, status.Errorf(codes.NotFound, "table %q not found", req.Name)
|
||||
}
|
||||
delete(s.tables, req.Name)
|
||||
return &emptypb.Empty{}, nil
|
||||
@@ -224,65 +226,103 @@ func (s *server) DropRowRange(ctx context.Context, req *btapb.DropRowRangeReques
|
||||
defer s.mu.Unlock()
|
||||
tbl, ok := s.tables[req.Name]
|
||||
if !ok {
|
||||
return nil, grpc.Errorf(codes.NotFound, "table %q not found", req.Name)
|
||||
return nil, status.Errorf(codes.NotFound, "table %q not found", req.Name)
|
||||
}
|
||||
|
||||
if req.GetDeleteAllDataFromTable() {
|
||||
tbl.rows = nil
|
||||
tbl.rowIndex = make(map[string]*row)
|
||||
tbl.rows = btree.New(btreeDegree)
|
||||
} else {
|
||||
// Delete rows by prefix
|
||||
// Delete rows by prefix.
|
||||
prefixBytes := req.GetRowKeyPrefix()
|
||||
if prefixBytes == nil {
|
||||
return nil, fmt.Errorf("missing row key prefix")
|
||||
}
|
||||
prefix := string(prefixBytes)
|
||||
|
||||
start := -1
|
||||
end := 0
|
||||
for i, row := range tbl.rows {
|
||||
match := strings.HasPrefix(row.key, prefix)
|
||||
if match {
|
||||
// Delete the mapping. Row will be deleted from sorted range below.
|
||||
delete(tbl.rowIndex, row.key)
|
||||
// The BTree does not specify what happens if rows are deleted during
|
||||
// iteration, and it provides no "delete range" method.
|
||||
// So we collect the rows first, then delete them one by one.
|
||||
var rowsToDelete []*row
|
||||
tbl.rows.AscendGreaterOrEqual(btreeKey(prefix), func(i btree.Item) bool {
|
||||
r := i.(*row)
|
||||
if strings.HasPrefix(r.key, prefix) {
|
||||
rowsToDelete = append(rowsToDelete, r)
|
||||
return true
|
||||
} else {
|
||||
return false // stop iteration
|
||||
}
|
||||
if match && start == -1 {
|
||||
start = i
|
||||
} else if !match && start != -1 {
|
||||
break
|
||||
}
|
||||
end++
|
||||
}
|
||||
if start != -1 {
|
||||
// Delete the range, using method from https://github.com/golang/go/wiki/SliceTricks
|
||||
copy(tbl.rows[start:], tbl.rows[end:])
|
||||
for k, n := len(tbl.rows)-end+start, len(tbl.rows); k < n; k++ {
|
||||
tbl.rows[k] = nil
|
||||
}
|
||||
tbl.rows = tbl.rows[:len(tbl.rows)-end+start]
|
||||
})
|
||||
for _, r := range rowsToDelete {
|
||||
tbl.rows.Delete(r)
|
||||
}
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
// This is a private alpha release of Cloud Bigtable replication. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (s *server) GenerateConsistencyToken(ctx context.Context, req *btapb.GenerateConsistencyTokenRequest) (*btapb.GenerateConsistencyTokenResponse, error) {
|
||||
// Check that the table exists.
|
||||
_, ok := s.tables[req.Name]
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "table %q not found", req.Name)
|
||||
}
|
||||
|
||||
return &btapb.GenerateConsistencyTokenResponse{
|
||||
ConsistencyToken: "TokenFor-" + req.Name,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// This is a private alpha release of Cloud Bigtable replication. This feature
|
||||
// is not currently available to most Cloud Bigtable customers. This feature
|
||||
// might be changed in backward-incompatible ways and is not recommended for
|
||||
// production use. It is not subject to any SLA or deprecation policy.
|
||||
func (s *server) CheckConsistency(ctx context.Context, req *btapb.CheckConsistencyRequest) (*btapb.CheckConsistencyResponse, error) {
|
||||
// Check that the table exists.
|
||||
_, ok := s.tables[req.Name]
|
||||
if !ok {
|
||||
return nil, status.Errorf(codes.NotFound, "table %q not found", req.Name)
|
||||
}
|
||||
|
||||
// Check this is the right token.
|
||||
if req.ConsistencyToken != "TokenFor-"+req.Name {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "token %q not valid", req.ConsistencyToken)
|
||||
}
|
||||
|
||||
// Single cluster instances are always consistent.
|
||||
return &btapb.CheckConsistencyResponse{
|
||||
Consistent: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) ReadRows(req *btpb.ReadRowsRequest, stream btpb.Bigtable_ReadRowsServer) error {
|
||||
s.mu.Lock()
|
||||
tbl, ok := s.tables[req.TableName]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return grpc.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
return status.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
}
|
||||
|
||||
// Rows to read can be specified by a set of row keys and/or a set of row ranges.
|
||||
// Output is a stream of sorted, de-duped rows.
|
||||
tbl.mu.RLock()
|
||||
rowSet := make(map[string]*row)
|
||||
|
||||
addRow := func(i btree.Item) bool {
|
||||
r := i.(*row)
|
||||
rowSet[r.key] = r
|
||||
return true
|
||||
}
|
||||
|
||||
if req.Rows != nil {
|
||||
// Add the explicitly given keys
|
||||
for _, key := range req.Rows.RowKeys {
|
||||
start := string(key)
|
||||
addRows(start, start+"\x00", tbl, rowSet)
|
||||
k := string(key)
|
||||
if i := tbl.rows.Get(btreeKey(k)); i != nil {
|
||||
addRow(i)
|
||||
}
|
||||
}
|
||||
|
||||
// Add keys from row ranges
|
||||
@@ -300,12 +340,20 @@ func (s *server) ReadRows(req *btpb.ReadRowsRequest, stream btpb.Bigtable_ReadRo
|
||||
case *btpb.RowRange_EndKeyOpen:
|
||||
end = string(ek.EndKeyOpen)
|
||||
}
|
||||
|
||||
addRows(start, end, tbl, rowSet)
|
||||
switch {
|
||||
case start == "" && end == "":
|
||||
tbl.rows.Ascend(addRow) // all rows
|
||||
case start == "":
|
||||
tbl.rows.AscendLessThan(btreeKey(end), addRow)
|
||||
case end == "":
|
||||
tbl.rows.AscendGreaterOrEqual(btreeKey(start), addRow)
|
||||
default:
|
||||
tbl.rows.AscendRange(btreeKey(start), btreeKey(end), addRow)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Read all rows
|
||||
addRows("", "", tbl, rowSet)
|
||||
tbl.rows.Ascend(addRow)
|
||||
}
|
||||
tbl.mu.RUnlock()
|
||||
|
||||
@@ -332,21 +380,6 @@ func (s *server) ReadRows(req *btpb.ReadRowsRequest, stream btpb.Bigtable_ReadRo
|
||||
return nil
|
||||
}
|
||||
|
||||
func addRows(start, end string, tbl *table, rowSet map[string]*row) {
|
||||
si, ei := 0, len(tbl.rows) // half-open interval
|
||||
if start != "" {
|
||||
si = sort.Search(len(tbl.rows), func(i int) bool { return tbl.rows[i].key >= start })
|
||||
}
|
||||
if end != "" {
|
||||
ei = sort.Search(len(tbl.rows), func(i int) bool { return tbl.rows[i].key >= end })
|
||||
}
|
||||
if si < ei {
|
||||
for _, row := range tbl.rows[si:ei] {
|
||||
rowSet[row.key] = row
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// streamRow filters the given row and sends it via the given stream.
|
||||
// Returns true if at least one cell matched the filter and was streamed, false otherwise.
|
||||
func streamRow(stream btpb.Bigtable_ReadRowsServer, r *row, f *btpb.RowFilter) (bool, error) {
|
||||
@@ -421,12 +454,14 @@ func filterRow(f *btpb.RowFilter, r *row) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
var count int
|
||||
for _, fam := range r.families {
|
||||
for _, cs := range fam.cells {
|
||||
sort.Sort(byDescTS(cs))
|
||||
count += len(cs)
|
||||
}
|
||||
}
|
||||
return true
|
||||
return count > 0
|
||||
case *btpb.RowFilter_CellsPerColumnLimitFilter:
|
||||
lim := int(f.CellsPerColumnLimitFilter)
|
||||
for _, fam := range r.families {
|
||||
@@ -621,12 +656,11 @@ func (s *server) MutateRow(ctx context.Context, req *btpb.MutateRowRequest) (*bt
|
||||
tbl, ok := s.tables[req.TableName]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return nil, grpc.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
return nil, status.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
}
|
||||
fs := tbl.columnFamilies()
|
||||
r, _ := tbl.mutableRow(string(req.RowKey))
|
||||
r := tbl.mutableRow(string(req.RowKey))
|
||||
r.mu.Lock()
|
||||
defer tbl.resortRowIndex() // Make sure the row lock is released before this grabs the table lock
|
||||
defer r.mu.Unlock()
|
||||
if err := applyMutations(tbl, r, req.Mutations, fs); err != nil {
|
||||
return nil, err
|
||||
@@ -639,15 +673,14 @@ func (s *server) MutateRows(req *btpb.MutateRowsRequest, stream btpb.Bigtable_Mu
|
||||
tbl, ok := s.tables[req.TableName]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return grpc.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
return status.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
}
|
||||
res := &btpb.MutateRowsResponse{Entries: make([]*btpb.MutateRowsResponse_Entry, len(req.Entries))}
|
||||
|
||||
fs := tbl.columnFamilies()
|
||||
|
||||
defer tbl.resortRowIndex()
|
||||
for i, entry := range req.Entries {
|
||||
r, _ := tbl.mutableRow(string(entry.RowKey))
|
||||
r := tbl.mutableRow(string(entry.RowKey))
|
||||
r.mu.Lock()
|
||||
code, msg := int32(codes.OK), ""
|
||||
if err := applyMutations(tbl, r, entry.Mutations, fs); err != nil {
|
||||
@@ -669,13 +702,13 @@ func (s *server) CheckAndMutateRow(ctx context.Context, req *btpb.CheckAndMutate
|
||||
tbl, ok := s.tables[req.TableName]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return nil, grpc.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
return nil, status.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
}
|
||||
res := &btpb.CheckAndMutateRowResponse{}
|
||||
|
||||
fs := tbl.columnFamilies()
|
||||
|
||||
r, _ := tbl.mutableRow(string(req.RowKey))
|
||||
r := tbl.mutableRow(string(req.RowKey))
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@@ -690,16 +723,13 @@ func (s *server) CheckAndMutateRow(ctx context.Context, req *btpb.CheckAndMutate
|
||||
nr := r.copy()
|
||||
filterRow(req.PredicateFilter, nr)
|
||||
whichMut = !nr.isEmpty()
|
||||
// TODO(dsymonds): Figure out if this is supposed to be set
|
||||
// even when there's no predicate filter.
|
||||
res.PredicateMatched = whichMut
|
||||
}
|
||||
res.PredicateMatched = whichMut
|
||||
muts := req.FalseMutations
|
||||
if whichMut {
|
||||
muts = req.TrueMutations
|
||||
}
|
||||
|
||||
defer tbl.resortRowIndex()
|
||||
if err := applyMutations(tbl, r, muts, fs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -824,18 +854,15 @@ func (s *server) ReadModifyWriteRow(ctx context.Context, req *btpb.ReadModifyWri
|
||||
tbl, ok := s.tables[req.TableName]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return nil, grpc.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
return nil, status.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
}
|
||||
updates := make(map[string]cell) // copy of updated cells; keyed by full column name
|
||||
|
||||
fs := tbl.columnFamilies()
|
||||
|
||||
rowKey := string(req.RowKey)
|
||||
r, isNewRow := tbl.mutableRow(rowKey)
|
||||
r := tbl.mutableRow(rowKey)
|
||||
// This must be done before the row lock, acquired below, is released.
|
||||
if isNewRow {
|
||||
defer tbl.resortRowIndex()
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
// Assume all mutations apply to the most recent version of the cell.
|
||||
@@ -907,7 +934,8 @@ func (s *server) ReadModifyWriteRow(ctx context.Context, req *btpb.ReadModifyWri
|
||||
f.Columns = append(f.Columns, &btpb.Column{
|
||||
Qualifier: []byte(qual),
|
||||
Cells: []*btpb.Cell{{
|
||||
Value: cell.value,
|
||||
TimestampMicros: cell.ts,
|
||||
Value: cell.value,
|
||||
}},
|
||||
})
|
||||
}
|
||||
@@ -919,7 +947,7 @@ func (s *server) SampleRowKeys(req *btpb.SampleRowKeysRequest, stream btpb.Bigta
|
||||
tbl, ok := s.tables[req.TableName]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return grpc.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
return status.Errorf(codes.NotFound, "table %q not found", req.TableName)
|
||||
}
|
||||
|
||||
tbl.mu.RLock()
|
||||
@@ -928,20 +956,25 @@ func (s *server) SampleRowKeys(req *btpb.SampleRowKeysRequest, stream btpb.Bigta
|
||||
// The return value of SampleRowKeys is very loosely defined. Return at least the
|
||||
// final row key in the table and choose other row keys randomly.
|
||||
var offset int64
|
||||
for i, row := range tbl.rows {
|
||||
if i == len(tbl.rows)-1 || rand.Int31n(100) == 0 {
|
||||
var err error
|
||||
i := 0
|
||||
tbl.rows.Ascend(func(it btree.Item) bool {
|
||||
row := it.(*row)
|
||||
if i == tbl.rows.Len()-1 || rand.Int31n(100) == 0 {
|
||||
resp := &btpb.SampleRowKeysResponse{
|
||||
RowKey: []byte(row.key),
|
||||
OffsetBytes: offset,
|
||||
}
|
||||
err := stream.Send(resp)
|
||||
err = stream.Send(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
return false
|
||||
}
|
||||
}
|
||||
offset += int64(row.size())
|
||||
}
|
||||
return nil
|
||||
i++
|
||||
return true
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// needGC is invoked whenever the server needs gcloop running.
|
||||
@@ -986,10 +1019,11 @@ type table struct {
|
||||
mu sync.RWMutex
|
||||
counter uint64 // increment by 1 when a new family is created
|
||||
families map[string]*columnFamily // keyed by plain family name
|
||||
rows []*row // sorted by row key
|
||||
rowIndex map[string]*row // indexed by row key
|
||||
rows *btree.BTree // indexed by row key
|
||||
}
|
||||
|
||||
const btreeDegree = 16
|
||||
|
||||
func newTable(ctr *btapb.CreateTableRequest) *table {
|
||||
fams := make(map[string]*columnFamily)
|
||||
c := uint64(0)
|
||||
@@ -1006,7 +1040,7 @@ func newTable(ctr *btapb.CreateTableRequest) *table {
|
||||
return &table{
|
||||
families: fams,
|
||||
counter: c,
|
||||
rowIndex: make(map[string]*row),
|
||||
rows: btree.New(btreeDegree),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,31 +1059,26 @@ func (t *table) columnFamilies() map[string]*columnFamily {
|
||||
return cp
|
||||
}
|
||||
|
||||
func (t *table) mutableRow(row string) (mutRow *row, isNewRow bool) {
|
||||
func (t *table) mutableRow(key string) *row {
|
||||
bkey := btreeKey(key)
|
||||
// Try fast path first.
|
||||
t.mu.RLock()
|
||||
r := t.rowIndex[row]
|
||||
i := t.rows.Get(bkey)
|
||||
t.mu.RUnlock()
|
||||
if r != nil {
|
||||
return r, false
|
||||
if i != nil {
|
||||
return i.(*row)
|
||||
}
|
||||
|
||||
// We probably need to create the row.
|
||||
t.mu.Lock()
|
||||
r = t.rowIndex[row]
|
||||
if r == nil {
|
||||
r = newRow(row)
|
||||
t.rowIndex[row] = r
|
||||
t.rows = append(t.rows, r)
|
||||
defer t.mu.Unlock()
|
||||
i = t.rows.Get(bkey)
|
||||
if i != nil {
|
||||
return i.(*row)
|
||||
}
|
||||
t.mu.Unlock()
|
||||
return r, true
|
||||
}
|
||||
|
||||
func (t *table) resortRowIndex() {
|
||||
t.mu.Lock()
|
||||
sort.Sort(byRowKey(t.rows))
|
||||
t.mu.Unlock()
|
||||
r := newRow(key)
|
||||
t.rows.ReplaceOrInsert(r)
|
||||
return r
|
||||
}
|
||||
|
||||
func (t *table) gc() {
|
||||
@@ -1068,11 +1097,13 @@ func (t *table) gc() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, r := range t.rows {
|
||||
t.rows.Ascend(func(i btree.Item) bool {
|
||||
r := i.(*row)
|
||||
r.mu.Lock()
|
||||
r.gc(rules)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
type byRowKey []*row
|
||||
@@ -1176,6 +1207,14 @@ func (r *row) size() int {
|
||||
return size
|
||||
}
|
||||
|
||||
// Less implements btree.Less.
|
||||
func (r *row) Less(i btree.Item) bool {
|
||||
return r.key < i.(*row).key
|
||||
}
|
||||
|
||||
// btreeKey returns a row for use as a key into the BTree.
|
||||
func btreeKey(s string) *row { return &row{key: s} }
|
||||
|
||||
func (r *row) String() string {
|
||||
return r.key
|
||||
}
|
||||
|
||||
+116
-12
@@ -17,6 +17,7 @@ package bttest
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
@@ -26,7 +27,6 @@ import (
|
||||
btapb "google.golang.org/genproto/googleapis/bigtable/admin/v2"
|
||||
btpb "google.golang.org/genproto/googleapis/bigtable/v2"
|
||||
"google.golang.org/grpc"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func TestConcurrentMutationsReadModifyAndGC(t *testing.T) {
|
||||
@@ -271,7 +271,7 @@ func TestDropRowRange(t *testing.T) {
|
||||
}
|
||||
|
||||
doWrite()
|
||||
tblSize := len(tbl.rows)
|
||||
tblSize := tbl.rows.Len()
|
||||
req := &btapb.DropRowRangeRequest{
|
||||
Name: tblInfo.Name,
|
||||
Target: &btapb.DropRowRangeRequest_RowKeyPrefix{[]byte("AAA")},
|
||||
@@ -279,7 +279,7 @@ func TestDropRowRange(t *testing.T) {
|
||||
if _, err = s.DropRowRange(ctx, req); err != nil {
|
||||
t.Fatalf("Dropping first range: %v", err)
|
||||
}
|
||||
got, want := len(tbl.rows), tblSize-count
|
||||
got, want := tbl.rows.Len(), tblSize-count
|
||||
if got != want {
|
||||
t.Errorf("Row count after first drop: got %d (%v), want %d", got, tbl.rows, want)
|
||||
}
|
||||
@@ -291,7 +291,7 @@ func TestDropRowRange(t *testing.T) {
|
||||
if _, err = s.DropRowRange(ctx, req); err != nil {
|
||||
t.Fatalf("Dropping second range: %v", err)
|
||||
}
|
||||
got, want = len(tbl.rows), tblSize-(2*count)
|
||||
got, want = tbl.rows.Len(), tblSize-(2*count)
|
||||
if got != want {
|
||||
t.Errorf("Row count after second drop: got %d (%v), want %d", got, tbl.rows, want)
|
||||
}
|
||||
@@ -303,7 +303,7 @@ func TestDropRowRange(t *testing.T) {
|
||||
if _, err = s.DropRowRange(ctx, req); err != nil {
|
||||
t.Fatalf("Dropping invalid range: %v", err)
|
||||
}
|
||||
got, want = len(tbl.rows), tblSize-(2*count)
|
||||
got, want = tbl.rows.Len(), tblSize-(2*count)
|
||||
if got != want {
|
||||
t.Errorf("Row count after invalid drop: got %d (%v), want %d", got, tbl.rows, want)
|
||||
}
|
||||
@@ -315,7 +315,7 @@ func TestDropRowRange(t *testing.T) {
|
||||
if _, err = s.DropRowRange(ctx, req); err != nil {
|
||||
t.Fatalf("Dropping all data: %v", err)
|
||||
}
|
||||
got, want = len(tbl.rows), 0
|
||||
got, want = tbl.rows.Len(), 0
|
||||
if got != want {
|
||||
t.Errorf("Row count after drop all: got %d, want %d", got, want)
|
||||
}
|
||||
@@ -331,13 +331,13 @@ func TestDropRowRange(t *testing.T) {
|
||||
if _, err = s.DropRowRange(ctx, req); err != nil {
|
||||
t.Fatalf("Dropping all data: %v", err)
|
||||
}
|
||||
got, want = len(tbl.rows), 0
|
||||
got, want = tbl.rows.Len(), 0
|
||||
if got != want {
|
||||
t.Errorf("Row count after drop all: got %d, want %d", got, want)
|
||||
}
|
||||
|
||||
doWrite()
|
||||
got, want = len(tbl.rows), len(prefixes)
|
||||
got, want = tbl.rows.Len(), len(prefixes)
|
||||
if got != want {
|
||||
t.Errorf("Row count after rewrite: got %d, want %d", got, want)
|
||||
}
|
||||
@@ -350,7 +350,7 @@ func TestDropRowRange(t *testing.T) {
|
||||
t.Fatalf("Dropping range: %v", err)
|
||||
}
|
||||
doWrite()
|
||||
got, want = len(tbl.rows), len(prefixes)
|
||||
got, want = tbl.rows.Len(), len(prefixes)
|
||||
if got != want {
|
||||
t.Errorf("Row count after drop range: got %d, want %d", got, want)
|
||||
}
|
||||
@@ -366,6 +366,56 @@ func (s *MockReadRowsServer) Send(resp *btpb.ReadRowsResponse) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestReadRows(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := &server{
|
||||
tables: make(map[string]*table),
|
||||
}
|
||||
newTbl := btapb.Table{
|
||||
ColumnFamilies: map[string]*btapb.ColumnFamily{
|
||||
"cf0": {GcRule: &btapb.GcRule{Rule: &btapb.GcRule_MaxNumVersions{1}}},
|
||||
},
|
||||
}
|
||||
tblInfo, err := s.CreateTable(ctx, &btapb.CreateTableRequest{Parent: "cluster", TableId: "t", Table: &newTbl})
|
||||
if err != nil {
|
||||
t.Fatalf("Creating table: %v", err)
|
||||
}
|
||||
mreq := &btpb.MutateRowRequest{
|
||||
TableName: tblInfo.Name,
|
||||
RowKey: []byte("row"),
|
||||
Mutations: []*btpb.Mutation{{
|
||||
Mutation: &btpb.Mutation_SetCell_{&btpb.Mutation_SetCell{
|
||||
FamilyName: "cf0",
|
||||
ColumnQualifier: []byte("col"),
|
||||
TimestampMicros: 1000,
|
||||
Value: []byte{},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
if _, err := s.MutateRow(ctx, mreq); err != nil {
|
||||
t.Fatalf("Populating table: %v", err)
|
||||
}
|
||||
|
||||
for _, rowset := range []*btpb.RowSet{
|
||||
{RowKeys: [][]byte{[]byte("row")}},
|
||||
{RowRanges: []*btpb.RowRange{{StartKey: &btpb.RowRange_StartKeyClosed{[]byte("")}}}},
|
||||
{RowRanges: []*btpb.RowRange{{StartKey: &btpb.RowRange_StartKeyClosed{[]byte("r")}}}},
|
||||
{RowRanges: []*btpb.RowRange{{
|
||||
StartKey: &btpb.RowRange_StartKeyClosed{[]byte("")},
|
||||
EndKey: &btpb.RowRange_EndKeyOpen{[]byte("s")},
|
||||
}}},
|
||||
} {
|
||||
mock := &MockReadRowsServer{}
|
||||
req := &btpb.ReadRowsRequest{TableName: tblInfo.Name, Rows: rowset}
|
||||
if err = s.ReadRows(req, mock); err != nil {
|
||||
t.Fatalf("ReadRows error: %v", err)
|
||||
}
|
||||
if got, want := len(mock.responses), 1; got != want {
|
||||
t.Errorf("%+v: response count: got %d, want %d", rowset, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRowsOrder(t *testing.T) {
|
||||
s := &server{
|
||||
tables: make(map[string]*table),
|
||||
@@ -430,7 +480,7 @@ func TestReadRowsOrder(t *testing.T) {
|
||||
t.Fatal("Response count: got 0, want > 0")
|
||||
}
|
||||
if len(mock.responses[0].Chunks) != 27 {
|
||||
t.Fatal("Chunk count: got %d, want 27", len(mock.responses[0].Chunks))
|
||||
t.Fatalf("Chunk count: got %d, want 27", len(mock.responses[0].Chunks))
|
||||
}
|
||||
testOrder := func(ms *MockReadRowsServer) {
|
||||
var prevFam, prevCol string
|
||||
@@ -480,7 +530,7 @@ func TestReadRowsOrder(t *testing.T) {
|
||||
t.Fatal("Response count: got 0, want > 0")
|
||||
}
|
||||
if len(mock.responses[0].Chunks) != 18 {
|
||||
t.Fatal("Chunk count: got %d, want 18", len(mock.responses[0].Chunks))
|
||||
t.Fatalf("Chunk count: got %d, want 18", len(mock.responses[0].Chunks))
|
||||
}
|
||||
testOrder(mock)
|
||||
|
||||
@@ -511,7 +561,61 @@ func TestReadRowsOrder(t *testing.T) {
|
||||
t.Fatal("Response count: got 0, want > 0")
|
||||
}
|
||||
if len(mock.responses[0].Chunks) != 30 {
|
||||
t.Fatal("Chunk count: got %d, want 30", len(mock.responses[0].Chunks))
|
||||
t.Fatalf("Chunk count: got %d, want 30", len(mock.responses[0].Chunks))
|
||||
}
|
||||
testOrder(mock)
|
||||
}
|
||||
|
||||
func TestCheckAndMutateRowWithoutPredicate(t *testing.T) {
|
||||
s := &server{
|
||||
tables: make(map[string]*table),
|
||||
}
|
||||
ctx := context.Background()
|
||||
newTbl := btapb.Table{
|
||||
ColumnFamilies: map[string]*btapb.ColumnFamily{
|
||||
"cf": {GcRule: &btapb.GcRule{Rule: &btapb.GcRule_MaxNumVersions{1}}},
|
||||
},
|
||||
}
|
||||
tbl, err := s.CreateTable(ctx, &btapb.CreateTableRequest{Parent: "cluster", TableId: "t", Table: &newTbl})
|
||||
if err != nil {
|
||||
t.Fatalf("Creating table: %v", err)
|
||||
}
|
||||
|
||||
// Populate the table
|
||||
val := []byte("value")
|
||||
mrreq := &btpb.MutateRowRequest{
|
||||
TableName: tbl.Name,
|
||||
RowKey: []byte("row-present"),
|
||||
Mutations: []*btpb.Mutation{{
|
||||
Mutation: &btpb.Mutation_SetCell_{&btpb.Mutation_SetCell{
|
||||
FamilyName: "cf",
|
||||
ColumnQualifier: []byte("col"),
|
||||
TimestampMicros: 0,
|
||||
Value: val,
|
||||
}},
|
||||
}},
|
||||
}
|
||||
if _, err := s.MutateRow(ctx, mrreq); err != nil {
|
||||
t.Fatalf("Populating table: %v", err)
|
||||
}
|
||||
|
||||
req := &btpb.CheckAndMutateRowRequest{
|
||||
TableName: tbl.Name,
|
||||
RowKey: []byte("row-not-present"),
|
||||
}
|
||||
if res, err := s.CheckAndMutateRow(ctx, req); err != nil {
|
||||
t.Errorf("CheckAndMutateRow error: %v", err)
|
||||
} else if got, want := res.PredicateMatched, false; got != want {
|
||||
t.Errorf("Invalid PredicateMatched value: got %t, want %t", got, want)
|
||||
}
|
||||
|
||||
req = &btpb.CheckAndMutateRowRequest{
|
||||
TableName: tbl.Name,
|
||||
RowKey: []byte("row-present"),
|
||||
}
|
||||
if res, err := s.CheckAndMutateRow(ctx, req); err != nil {
|
||||
t.Errorf("CheckAndMutateRow error: %v", err)
|
||||
} else if got, want := res.PredicateMatched, true; got != want {
|
||||
t.Errorf("Invalid PredicateMatched value: got %t, want %t", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
+527
-51
@@ -34,9 +34,12 @@ import (
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"encoding/csv"
|
||||
|
||||
"cloud.google.com/go/bigtable"
|
||||
"cloud.google.com/go/bigtable/internal/cbtconfig"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
@@ -64,7 +67,7 @@ func getCredentialOpts(opts []option.ClientOption) []option.ClientOption {
|
||||
return opts
|
||||
}
|
||||
|
||||
func getClient() *bigtable.Client {
|
||||
func getClient(clientConf bigtable.ClientConfig) *bigtable.Client {
|
||||
if client == nil {
|
||||
var opts []option.ClientOption
|
||||
if ep := config.DataEndpoint; ep != "" {
|
||||
@@ -72,7 +75,7 @@ func getClient() *bigtable.Client {
|
||||
}
|
||||
opts = getCredentialOpts(opts)
|
||||
var err error
|
||||
client, err = bigtable.NewClient(context.Background(), config.Project, config.Instance, opts...)
|
||||
client, err = bigtable.NewClientWithConfig(context.Background(), config.Project, config.Instance, clientConf, opts...)
|
||||
if err != nil {
|
||||
log.Fatalf("Making bigtable.Client: %v", err)
|
||||
}
|
||||
@@ -170,10 +173,15 @@ func init() {
|
||||
}
|
||||
tw.Flush()
|
||||
buf.WriteString(configHelp)
|
||||
buf.WriteString("\ncbt ` + version + ` ` + revision + ` ` + revisionDate + `")
|
||||
cmdSummary = buf.String()
|
||||
}
|
||||
|
||||
var configHelp = `
|
||||
Alpha features are not currently available to most Cloud Bigtable customers. The
|
||||
features might be changed in backward-incompatible ways and are not recommended
|
||||
for production use. They are not subject to any SLA or deprecation policy.
|
||||
|
||||
For convenience, values of the -project, -instance, -creds,
|
||||
-admin-endpoint and -data-endpoint flags may be specified in
|
||||
` + cbtconfig.Filename() + ` in this format:
|
||||
@@ -183,8 +191,6 @@ For convenience, values of the -project, -instance, -creds,
|
||||
admin-endpoint = hostname:port
|
||||
data-endpoint = hostname:port
|
||||
All values are optional, and all will be overridden by flags.
|
||||
|
||||
cbt ` + version + ` ` + revision + ` ` + revisionDate + `
|
||||
`
|
||||
|
||||
var commands = []struct {
|
||||
@@ -200,6 +206,30 @@ var commands = []struct {
|
||||
Usage: "cbt count <table>",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "createinstance",
|
||||
Desc: "Create an instance with an initial cluster",
|
||||
do: doCreateInstance,
|
||||
Usage: "cbt createinstance <instance-id> <display-name> <cluster-id> <zone> <num-nodes> <storage type>\n" +
|
||||
" instance-id Permanent, unique id for the instance\n" +
|
||||
" display-name Description of the instance\n" +
|
||||
" cluster-id Permanent, unique id for the cluster in the instance\n" +
|
||||
" zone The zone in which to create the cluster\n" +
|
||||
" num-nodes The number of nodes to create\n" +
|
||||
" storage-type SSD or HDD\n",
|
||||
Required: cbtconfig.ProjectRequired,
|
||||
},
|
||||
{
|
||||
Name: "createcluster",
|
||||
Desc: "Create a cluster in the configured instance (replication alpha)",
|
||||
do: doCreateCluster,
|
||||
Usage: "cbt createcluster <cluster-id> <zone> <num-nodes> <storage type>\n" +
|
||||
" cluster-id Permanent, unique id for the cluster in the instance\n" +
|
||||
" zone The zone in which to create the cluster\n" +
|
||||
" num-nodes The number of nodes to create\n" +
|
||||
" storage-type SSD or HDD\n",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "createfamily",
|
||||
Desc: "Create a column family",
|
||||
@@ -211,9 +241,41 @@ var commands = []struct {
|
||||
Name: "createtable",
|
||||
Desc: "Create a table",
|
||||
do: doCreateTable,
|
||||
Usage: "cbt createtable <table> [initial_splits...]\n" +
|
||||
" initial_splits=row A row key to be used to initially split the table " +
|
||||
"into multiple tablets. Can be repeated to create multiple splits.",
|
||||
Usage: "cbt createtable <table> [families=family[:(maxage=<d> | maxversions=<n>)],...] [splits=split,...]\n" +
|
||||
" families: Column families and their associated GC policies. See \"setgcpolicy\".\n" +
|
||||
" Example: families=family1:maxage=1w,family2:maxversions=1\n" +
|
||||
" splits: Row key to be used to initially split the table",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "updatecluster",
|
||||
Desc: "Update a cluster in the configured instance",
|
||||
do: doUpdateCluster,
|
||||
Usage: "cbt updatecluster <cluster-id> [num-nodes=num-nodes]\n" +
|
||||
" cluster-id Permanent, unique id for the cluster in the instance\n" +
|
||||
" num-nodes The number of nodes to update to",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "deleteinstance",
|
||||
Desc: "Deletes an instance",
|
||||
do: doDeleteInstance,
|
||||
Usage: "cbt deleteinstance <instance>",
|
||||
Required: cbtconfig.ProjectRequired,
|
||||
},
|
||||
{
|
||||
Name: "deletecluster",
|
||||
Desc: "Deletes a cluster from the configured instance (replication alpha)",
|
||||
do: doDeleteCluster,
|
||||
Usage: "cbt deletecluster <cluster>",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "deletecolumn",
|
||||
Desc: "Delete all cells in a column",
|
||||
do: doDeleteColumn,
|
||||
Usage: "cbt deletecolumn <table> <row> <family> <column> [app-profile=<app profile id>]\n" +
|
||||
" app-profile=<app profile id> The app profile id to use for the request (replication alpha)\n",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
@@ -224,10 +286,11 @@ var commands = []struct {
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "deleterow",
|
||||
Desc: "Delete a row",
|
||||
do: doDeleteRow,
|
||||
Usage: "cbt deleterow <table> <row>",
|
||||
Name: "deleterow",
|
||||
Desc: "Delete a row",
|
||||
do: doDeleteRow,
|
||||
Usage: "cbt deleterow <table> <row> [app-profile=<app profile id>]\n" +
|
||||
" app-profile=<app profile id> The app profile id to use for the request (replication alpha)\n",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
@@ -259,10 +322,18 @@ var commands = []struct {
|
||||
Required: cbtconfig.ProjectRequired,
|
||||
},
|
||||
{
|
||||
Name: "lookup",
|
||||
Desc: "Read from a single row",
|
||||
do: doLookup,
|
||||
Usage: "cbt lookup <table> <row>",
|
||||
Name: "listclusters",
|
||||
Desc: "List instances in an instance",
|
||||
do: doListClusters,
|
||||
Usage: "cbt listclusters",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "lookup",
|
||||
Desc: "Read from a single row",
|
||||
do: doLookup,
|
||||
Usage: "cbt lookup <table> <row> [app-profile=<app profile id>]\n" +
|
||||
" app-profile=<app profile id> The app profile id to use for the request (replication alpha)\n",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
@@ -284,18 +355,22 @@ var commands = []struct {
|
||||
Name: "read",
|
||||
Desc: "Read rows",
|
||||
do: doRead,
|
||||
Usage: "cbt read <table> [start=<row>] [end=<row>] [prefix=<prefix>] [count=<n>]\n" +
|
||||
Usage: "cbt read <table> [start=<row>] [end=<row>] [prefix=<prefix>]" +
|
||||
" [regex=<regex>] [count=<n>] [app-profile=<app profile id>]\n" +
|
||||
" start=<row> Start reading at this row\n" +
|
||||
" end=<row> Stop reading before this row\n" +
|
||||
" prefix=<prefix> Read rows with this prefix\n" +
|
||||
" count=<n> Read only this many rows\n",
|
||||
" regex=<regex> Read rows with keys matching this regex\n" +
|
||||
" count=<n> Read only this many rows\n" +
|
||||
" app-profile=<app profile id> The app profile id to use for the request (replication alpha)\n",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "set",
|
||||
Desc: "Set value of a cell",
|
||||
do: doSet,
|
||||
Usage: "cbt set <table> <row> family:column=val[@ts] ...\n" +
|
||||
Usage: "cbt set <table> <row> [app-profile=<app profile id>] family:column=val[@ts] ...\n" +
|
||||
" app-profile=<app profile id> The app profile id to use for the request (replication alpha)\n" +
|
||||
" family:column=val[@ts] may be repeated to set multiple cells.\n" +
|
||||
"\n" +
|
||||
" ts is an optional integer timestamp.\n" +
|
||||
@@ -313,6 +388,53 @@ var commands = []struct {
|
||||
" maxversions=<n> Maximum number of versions to preserve",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "waitforreplication",
|
||||
Desc: "Blocks until all the completed writes have been replicated to all the clusters (replication alpha)",
|
||||
do: doWaitForReplicaiton,
|
||||
Usage: "cbt waitforreplication <table>",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "createtablefromsnapshot",
|
||||
Desc: "Create a table from a snapshot (snapshots alpha)",
|
||||
do: doCreateTableFromSnapshot,
|
||||
Usage: "cbt createtablefromsnapshot <table> <cluster> <snapshot>\n" +
|
||||
" table The name of the table to create\n" +
|
||||
" cluster The cluster where the snapshot is located\n" +
|
||||
" snapshot The snapshot to restore",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "createsnapshot",
|
||||
Desc: "Create a snapshot from a source table (snapshots alpha)",
|
||||
do: doSnapshotTable,
|
||||
Usage: "cbt createsnapshot <cluster> <snapshot> <table> [ttl=<d>]\n" +
|
||||
"\n" +
|
||||
` [ttl=<d>] Lifespan of the snapshot (e.g. "1h", "4d")` + "\n",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "listsnapshots",
|
||||
Desc: "List snapshots in a cluster (snapshots alpha)",
|
||||
do: doListSnapshots,
|
||||
Usage: "cbt listsnapshots [<cluster>]",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "getsnapshot",
|
||||
Desc: "Get snapshot info (snapshots alpha)",
|
||||
do: doGetSnapshot,
|
||||
Usage: "cbt getsnapshot <cluster> <snapshot>",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "deletesnapshot",
|
||||
Desc: "Delete snapshot in a cluster (snapshots alpha)",
|
||||
do: doDeleteSnapshot,
|
||||
Usage: "cbt deletesnapshot <cluster> <snapshot>",
|
||||
Required: cbtconfig.ProjectAndInstanceRequired,
|
||||
},
|
||||
{
|
||||
Name: "version",
|
||||
Desc: "Print the current cbt version",
|
||||
@@ -326,7 +448,7 @@ func doCount(ctx context.Context, args ...string) {
|
||||
if len(args) != 1 {
|
||||
log.Fatal("usage: cbt count <table>")
|
||||
}
|
||||
tbl := getClient().Open(args[0])
|
||||
tbl := getClient(bigtable.ClientConfig{}).Open(args[0])
|
||||
|
||||
n := 0
|
||||
err := tbl.ReadRows(ctx, bigtable.InfiniteRange(""), func(_ bigtable.Row) bool {
|
||||
@@ -339,6 +461,51 @@ func doCount(ctx context.Context, args ...string) {
|
||||
fmt.Println(n)
|
||||
}
|
||||
|
||||
func doCreateTable(ctx context.Context, args ...string) {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("usage: cbt createtable <table> [families=family[:gcpolicy],...] [splits=split,...]")
|
||||
}
|
||||
|
||||
tblConf := bigtable.TableConf{TableID: args[0]}
|
||||
for _, arg := range args[1:] {
|
||||
i := strings.Index(arg, "=")
|
||||
if i < 0 {
|
||||
log.Fatalf("Bad arg %q", arg)
|
||||
}
|
||||
key, val := arg[:i], arg[i+1:]
|
||||
chunks, err := csv.NewReader(strings.NewReader(val)).Read()
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid families arg format: %v", err)
|
||||
}
|
||||
switch key {
|
||||
default:
|
||||
log.Fatalf("Unknown arg key %q", key)
|
||||
case "families":
|
||||
tblConf.Families = make(map[string]bigtable.GCPolicy)
|
||||
for _, family := range chunks {
|
||||
famPolicy := strings.Split(family, ":")
|
||||
var gcPolicy bigtable.GCPolicy
|
||||
if len(famPolicy) < 2 {
|
||||
gcPolicy = bigtable.MaxVersionsPolicy(1)
|
||||
log.Printf("Using default GC Policy of %v for family %v", gcPolicy, family)
|
||||
} else {
|
||||
gcPolicy, err = parseGCPolicy(famPolicy[1])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
tblConf.Families[famPolicy[0]] = gcPolicy
|
||||
}
|
||||
case "splits":
|
||||
tblConf.SplitKeys = chunks
|
||||
}
|
||||
}
|
||||
|
||||
if err := getAdminClient().CreateTableFromConf(ctx, &tblConf); err != nil {
|
||||
log.Fatalf("Creating table: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doCreateFamily(ctx context.Context, args ...string) {
|
||||
if len(args) != 2 {
|
||||
log.Fatal("usage: cbt createfamily <table> <family>")
|
||||
@@ -349,19 +516,135 @@ func doCreateFamily(ctx context.Context, args ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
func doCreateTable(ctx context.Context, args ...string) {
|
||||
if len(args) < 1 {
|
||||
log.Fatal("usage: cbt createtable <table> [initial_splits...]")
|
||||
}
|
||||
var err error
|
||||
if len(args) > 1 {
|
||||
splits := args[1:]
|
||||
err = getAdminClient().CreatePresplitTable(ctx, args[0], splits)
|
||||
} else {
|
||||
err = getAdminClient().CreateTable(ctx, args[0])
|
||||
func doCreateInstance(ctx context.Context, args ...string) {
|
||||
if len(args) < 6 {
|
||||
log.Fatal("cbt createinstance <instance-id> <display-name> <cluster-id> <zone> <num-nodes> <storage type>")
|
||||
}
|
||||
|
||||
numNodes, err := strconv.ParseInt(args[4], 0, 32)
|
||||
if err != nil {
|
||||
log.Fatalf("Creating table: %v", err)
|
||||
log.Fatalf("Bad num-nodes %q: %v", args[4], err)
|
||||
}
|
||||
|
||||
sType, err := parseStorageType(args[5])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
ic := bigtable.InstanceWithClustersConfig{
|
||||
InstanceID: args[0],
|
||||
DisplayName: args[1],
|
||||
Clusters: []bigtable.ClusterConfig{{
|
||||
ClusterID: args[2],
|
||||
Zone: args[3],
|
||||
NumNodes: int32(numNodes),
|
||||
StorageType: sType,
|
||||
}},
|
||||
}
|
||||
err = getInstanceAdminClient().CreateInstanceWithClusters(ctx, &ic)
|
||||
if err != nil {
|
||||
log.Fatalf("Creating instance: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doCreateCluster(ctx context.Context, args ...string) {
|
||||
if len(args) < 4 {
|
||||
log.Fatal("usage: cbt createcluster <cluster-id> <zone> <num-nodes> <storage type>")
|
||||
}
|
||||
|
||||
numNodes, err := strconv.ParseInt(args[2], 0, 32)
|
||||
if err != nil {
|
||||
log.Fatalf("Bad num_nodes %q: %v", args[2], err)
|
||||
}
|
||||
|
||||
sType, err := parseStorageType(args[3])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cc := bigtable.ClusterConfig{
|
||||
InstanceID: config.Instance,
|
||||
ClusterID: args[0],
|
||||
Zone: args[1],
|
||||
NumNodes: int32(numNodes),
|
||||
StorageType: sType,
|
||||
}
|
||||
err = getInstanceAdminClient().CreateCluster(ctx, &cc)
|
||||
if err != nil {
|
||||
log.Fatalf("Creating cluster: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doUpdateCluster(ctx context.Context, args ...string) {
|
||||
if len(args) < 2 {
|
||||
log.Fatal("cbt updatecluster <cluster-id> [num-nodes=num-nodes]")
|
||||
}
|
||||
|
||||
numNodes := int64(0)
|
||||
var err error
|
||||
for _, arg := range args[1:] {
|
||||
i := strings.Index(arg, "=")
|
||||
if i < 0 {
|
||||
log.Fatalf("Bad arg %q", arg)
|
||||
}
|
||||
key, val := arg[:i], arg[i+1:]
|
||||
switch key {
|
||||
default:
|
||||
log.Fatalf("Unknown arg key %q", key)
|
||||
case "num-nodes":
|
||||
numNodes, err = strconv.ParseInt(val, 0, 32)
|
||||
if err != nil {
|
||||
log.Fatalf("Bad num-nodes %q: %v", val, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
if numNodes > 0 {
|
||||
err = getInstanceAdminClient().UpdateCluster(ctx, config.Instance, args[0], int32(numNodes))
|
||||
if err != nil {
|
||||
log.Fatalf("Updating cluster: %v", err)
|
||||
}
|
||||
} else {
|
||||
log.Fatal("Updating cluster: nothing to update")
|
||||
}
|
||||
}
|
||||
|
||||
func doDeleteInstance(ctx context.Context, args ...string) {
|
||||
if len(args) != 1 {
|
||||
log.Fatal("usage: cbt deleteinstance <instance>")
|
||||
}
|
||||
err := getInstanceAdminClient().DeleteInstance(ctx, args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Deleting instance: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doDeleteCluster(ctx context.Context, args ...string) {
|
||||
if len(args) != 1 {
|
||||
log.Fatal("usage: cbt deletecluster <cluster>")
|
||||
}
|
||||
err := getInstanceAdminClient().DeleteCluster(ctx, config.Instance, args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("Deleting cluster: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doDeleteColumn(ctx context.Context, args ...string) {
|
||||
usage := "usage: cbt deletecolumn <table> <row> <family> <column> [app-profile=<app profile id>]"
|
||||
if len(args) != 4 || len(args) != 5 {
|
||||
log.Fatal(usage)
|
||||
}
|
||||
var appProfile string
|
||||
if len(args) == 5 {
|
||||
if !strings.HasPrefix(args[4], "app-profile=") {
|
||||
log.Fatal(usage)
|
||||
}
|
||||
appProfile = strings.Split(args[4], "=")[1]
|
||||
}
|
||||
tbl := getClient(bigtable.ClientConfig{AppProfile: appProfile}).Open(args[0])
|
||||
mut := bigtable.NewMutation()
|
||||
mut.DeleteCellsInColumn(args[2], args[3])
|
||||
if err := tbl.Apply(ctx, args[1], mut); err != nil {
|
||||
log.Fatalf("Deleting cells in column: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -376,10 +659,18 @@ func doDeleteFamily(ctx context.Context, args ...string) {
|
||||
}
|
||||
|
||||
func doDeleteRow(ctx context.Context, args ...string) {
|
||||
if len(args) != 2 {
|
||||
log.Fatal("usage: cbt deleterow <table> <row>")
|
||||
usage := "usage: cbt deleterow <table> <row> [app-profile=<app profile id>]"
|
||||
if len(args) != 2 || len(args) != 3 {
|
||||
log.Fatal(usage)
|
||||
}
|
||||
tbl := getClient().Open(args[0])
|
||||
var appProfile string
|
||||
if len(args) == 3 {
|
||||
if !strings.HasPrefix(args[2], "app-profile=") {
|
||||
log.Fatal(usage)
|
||||
}
|
||||
appProfile = strings.Split(args[2], "=")[1]
|
||||
}
|
||||
tbl := getClient(bigtable.ClientConfig{AppProfile: appProfile}).Open(args[0])
|
||||
mut := bigtable.NewMutation()
|
||||
mut.DeleteRow()
|
||||
if err := tbl.Apply(ctx, args[1], mut); err != nil {
|
||||
@@ -429,8 +720,9 @@ func docFlags() []*flag.Flag {
|
||||
|
||||
func doDocReal(ctx context.Context, args ...string) {
|
||||
data := map[string]interface{}{
|
||||
"Commands": commands,
|
||||
"Flags": docFlags(),
|
||||
"Commands": commands,
|
||||
"Flags": docFlags(),
|
||||
"ConfigHelp": configHelp,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := docTemplate.Execute(&buf, data); err != nil {
|
||||
@@ -474,7 +766,9 @@ var docTemplate = template.Must(template.New("doc").Funcs(template.FuncMap{
|
||||
//go:generate go run cbt.go -o cbtdoc.go doc
|
||||
|
||||
/*
|
||||
Cbt is a tool for doing basic interactions with Cloud Bigtable.
|
||||
Cbt is a tool for doing basic interactions with Cloud Bigtable. To learn how to
|
||||
install the cbt tool, see the
|
||||
[cbt overview](https://cloud.google.com/bigtable/docs/go/cbt-overview).
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -491,6 +785,8 @@ The options are:
|
||||
-{{.Name}} string
|
||||
{{.Usage}}{{end}}
|
||||
|
||||
{{.ConfigHelp}}
|
||||
|
||||
{{range .Commands}}
|
||||
{{.Desc}}
|
||||
|
||||
@@ -535,12 +831,37 @@ func doListInstances(ctx context.Context, args ...string) {
|
||||
tw.Flush()
|
||||
}
|
||||
|
||||
func doListClusters(ctx context.Context, args ...string) {
|
||||
if len(args) != 0 {
|
||||
log.Fatalf("usage: cbt listclusters")
|
||||
}
|
||||
cis, err := getInstanceAdminClient().Clusters(ctx, config.Instance)
|
||||
if err != nil {
|
||||
log.Fatalf("Getting list of clusters: %v", err)
|
||||
}
|
||||
tw := tabwriter.NewWriter(os.Stdout, 10, 8, 4, '\t', 0)
|
||||
fmt.Fprintf(tw, "Cluster Name\tZone\tState\n")
|
||||
fmt.Fprintf(tw, "------------\t----\t----\n")
|
||||
for _, ci := range cis {
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s (%d serve nodes)\n", ci.Name, ci.Zone, ci.State, ci.ServeNodes)
|
||||
}
|
||||
tw.Flush()
|
||||
}
|
||||
|
||||
func doLookup(ctx context.Context, args ...string) {
|
||||
if len(args) != 2 {
|
||||
log.Fatalf("usage: cbt lookup <table> <row>")
|
||||
if len(args) < 2 {
|
||||
log.Fatalf("usage: cbt lookup <table> <row> [app-profile=<app profile id>]")
|
||||
}
|
||||
var appProfile string
|
||||
if len(args) > 2 {
|
||||
i := strings.Index(args[2], "=")
|
||||
if i < 0 {
|
||||
log.Fatalf("Bad arg %q", args[2])
|
||||
}
|
||||
appProfile = strings.Split(args[2], "=")[1]
|
||||
}
|
||||
table, row := args[0], args[1]
|
||||
tbl := getClient().Open(table)
|
||||
tbl := getClient(bigtable.ClientConfig{AppProfile: appProfile}).Open(table)
|
||||
r, err := tbl.ReadRow(ctx, row)
|
||||
if err != nil {
|
||||
log.Fatalf("Reading row: %v", err)
|
||||
@@ -612,8 +933,9 @@ func doLS(ctx context.Context, args ...string) {
|
||||
|
||||
func doMDDocReal(ctx context.Context, args ...string) {
|
||||
data := map[string]interface{}{
|
||||
"Commands": commands,
|
||||
"Flags": docFlags(),
|
||||
"Commands": commands,
|
||||
"Flags": docFlags(),
|
||||
"ConfigHelp": configHelp,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := mddocTemplate.Execute(&buf, data); err != nil {
|
||||
@@ -643,6 +965,8 @@ The options are:
|
||||
-{{.Name}} string
|
||||
{{.Usage}}{{end}}
|
||||
|
||||
{{.ConfigHelp}}
|
||||
|
||||
{{range .Commands}}
|
||||
## {{.Desc}}
|
||||
|
||||
@@ -657,7 +981,6 @@ func doRead(ctx context.Context, args ...string) {
|
||||
if len(args) < 1 {
|
||||
log.Fatalf("usage: cbt read <table> [args ...]")
|
||||
}
|
||||
tbl := getClient().Open(args[0])
|
||||
|
||||
parsed := make(map[string]string)
|
||||
for _, arg := range args[1:] {
|
||||
@@ -672,7 +995,7 @@ func doRead(ctx context.Context, args ...string) {
|
||||
case "limit":
|
||||
// Be nicer; we used to support this, but renamed it to "end".
|
||||
log.Fatalf("Unknown arg key %q; did you mean %q?", key, "end")
|
||||
case "start", "end", "prefix", "count":
|
||||
case "start", "end", "prefix", "count", "regex", "app-profile":
|
||||
parsed[key] = val
|
||||
}
|
||||
}
|
||||
@@ -698,8 +1021,12 @@ func doRead(ctx context.Context, args ...string) {
|
||||
}
|
||||
opts = append(opts, bigtable.LimitRows(n))
|
||||
}
|
||||
if regex := parsed["regex"]; regex != "" {
|
||||
opts = append(opts, bigtable.RowFilter(bigtable.RowKeyFilter(regex)))
|
||||
}
|
||||
|
||||
// TODO(dsymonds): Support filters.
|
||||
tbl := getClient(bigtable.ClientConfig{AppProfile: parsed["app-profile"]}).Open(args[0])
|
||||
err := tbl.ReadRows(ctx, rr, func(r bigtable.Row) bool {
|
||||
printRow(r)
|
||||
return true
|
||||
@@ -713,12 +1040,16 @@ var setArg = regexp.MustCompile(`([^:]+):([^=]*)=(.*)`)
|
||||
|
||||
func doSet(ctx context.Context, args ...string) {
|
||||
if len(args) < 3 {
|
||||
log.Fatalf("usage: cbt set <table> <row> family:[column]=val[@ts] ...")
|
||||
log.Fatalf("usage: cbt set <table> <row> [app-profile=<app profile id>] family:[column]=val[@ts] ...")
|
||||
}
|
||||
tbl := getClient().Open(args[0])
|
||||
var appProfile string
|
||||
row := args[1]
|
||||
mut := bigtable.NewMutation()
|
||||
for _, arg := range args[2:] {
|
||||
if strings.HasPrefix(arg, "app-profile=") {
|
||||
appProfile = strings.Split(arg, "=")[1]
|
||||
continue
|
||||
}
|
||||
m := setArg.FindStringSubmatch(arg)
|
||||
if m == nil {
|
||||
log.Fatalf("Bad set arg %q", arg)
|
||||
@@ -735,6 +1066,7 @@ func doSet(ctx context.Context, args ...string) {
|
||||
}
|
||||
mut.Set(m[1], m[2], ts, []byte(val))
|
||||
}
|
||||
tbl := getClient(bigtable.ClientConfig{AppProfile: appProfile}).Open(args[0])
|
||||
if err := tbl.Apply(ctx, row, mut); err != nil {
|
||||
log.Fatalf("Applying mutation: %v", err)
|
||||
}
|
||||
@@ -747,25 +1079,169 @@ func doSetGCPolicy(ctx context.Context, args ...string) {
|
||||
table := args[0]
|
||||
fam := args[1]
|
||||
|
||||
pol, err := parseGCPolicy(args[2])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := getAdminClient().SetGCPolicy(ctx, table, fam, pol); err != nil {
|
||||
log.Fatalf("Setting GC policy: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doWaitForReplicaiton(ctx context.Context, args ...string) {
|
||||
if len(args) != 1 {
|
||||
log.Fatalf("usage: cbt waitforreplication <table>")
|
||||
}
|
||||
table := args[0]
|
||||
|
||||
fmt.Printf("Waiting for all writes up to %s to be replicated.\n", time.Now().Format("2006/01/02-15:04:05"))
|
||||
if err := getAdminClient().WaitForReplication(ctx, table); err != nil {
|
||||
log.Fatalf("Waiting for replication: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func parseGCPolicy(policyStr string) (bigtable.GCPolicy, error) {
|
||||
var pol bigtable.GCPolicy
|
||||
switch p := args[2]; {
|
||||
switch p := policyStr; {
|
||||
case strings.HasPrefix(p, "maxage="):
|
||||
d, err := parseDuration(p[7:])
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
pol = bigtable.MaxAgePolicy(d)
|
||||
case strings.HasPrefix(p, "maxversions="):
|
||||
n, err := strconv.ParseUint(p[12:], 10, 16)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
pol = bigtable.MaxVersionsPolicy(int(n))
|
||||
default:
|
||||
log.Fatalf("Bad GC policy %q", p)
|
||||
return nil, fmt.Errorf("Bad GC policy %q", p)
|
||||
}
|
||||
if err := getAdminClient().SetGCPolicy(ctx, table, fam, pol); err != nil {
|
||||
log.Fatalf("Setting GC policy: %v", err)
|
||||
return pol, nil
|
||||
}
|
||||
|
||||
func parseStorageType(storageTypeStr string) (bigtable.StorageType, error) {
|
||||
switch storageTypeStr {
|
||||
case "SSD":
|
||||
return bigtable.SSD, nil
|
||||
case "HDD":
|
||||
return bigtable.HDD, nil
|
||||
}
|
||||
return -1, fmt.Errorf("Invalid storage type: %v, must be SSD or HDD", storageTypeStr)
|
||||
}
|
||||
|
||||
func doCreateTableFromSnapshot(ctx context.Context, args ...string) {
|
||||
if len(args) != 3 {
|
||||
log.Fatal("usage: cbt createtablefromsnapshot <table> <cluster> <snapshot>")
|
||||
}
|
||||
tableName := args[0]
|
||||
clusterName := args[1]
|
||||
snapshotName := args[2]
|
||||
err := getAdminClient().CreateTableFromSnapshot(ctx, tableName, clusterName, snapshotName)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Creating table: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doSnapshotTable(ctx context.Context, args ...string) {
|
||||
if len(args) != 3 && len(args) != 4 {
|
||||
log.Fatal("usage: cbt createsnapshot <cluster> <snapshot> <table> [ttl=<d>]")
|
||||
}
|
||||
clusterName := args[0]
|
||||
snapshotName := args[1]
|
||||
tableName := args[2]
|
||||
ttl := bigtable.DefaultSnapshotDuration
|
||||
|
||||
for _, arg := range args[3:] {
|
||||
i := strings.Index(arg, "=")
|
||||
if i < 0 {
|
||||
log.Fatalf("Bad arg %q", arg)
|
||||
}
|
||||
key, val := arg[:i], arg[i+1:]
|
||||
switch key {
|
||||
default:
|
||||
log.Fatalf("Unknown arg key %q", key)
|
||||
case "ttl":
|
||||
var err error
|
||||
ttl, err = parseDuration(val)
|
||||
if err != nil {
|
||||
log.Fatalf("Invalid snapshot ttl value %q: %v", val, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err := getAdminClient().SnapshotTable(ctx, tableName, clusterName, snapshotName, ttl)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create Snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func doListSnapshots(ctx context.Context, args ...string) {
|
||||
if len(args) != 0 && len(args) != 1 {
|
||||
log.Fatal("usage: cbt listsnapshots [<cluster>]")
|
||||
}
|
||||
|
||||
var cluster string
|
||||
|
||||
if len(args) == 0 {
|
||||
cluster = "-"
|
||||
} else {
|
||||
cluster = args[0]
|
||||
}
|
||||
|
||||
it := getAdminClient().ListSnapshots(ctx, cluster)
|
||||
|
||||
tw := tabwriter.NewWriter(os.Stdout, 10, 8, 4, '\t', 0)
|
||||
fmt.Fprintf(tw, "Snapshot\tSource Table\tCreated At\tExpires At\n")
|
||||
fmt.Fprintf(tw, "--------\t------------\t----------\t----------\n")
|
||||
timeLayout := "2006-01-02 15:04 MST"
|
||||
|
||||
for {
|
||||
snapshot, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch snapshots %v", err)
|
||||
}
|
||||
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", snapshot.Name, snapshot.SourceTable, snapshot.CreateTime.Format(timeLayout), snapshot.DeleteTime.Format(timeLayout))
|
||||
}
|
||||
tw.Flush()
|
||||
}
|
||||
|
||||
func doGetSnapshot(ctx context.Context, args ...string) {
|
||||
if len(args) != 2 {
|
||||
log.Fatalf("usage: cbt getsnapshot <cluster> <snapshot>")
|
||||
}
|
||||
clusterName := args[0]
|
||||
snapshotName := args[1]
|
||||
|
||||
snapshot, err := getAdminClient().SnapshotInfo(ctx, clusterName, snapshotName)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get snapshot: %v", err)
|
||||
}
|
||||
|
||||
timeLayout := "2006-01-02 15:04 MST"
|
||||
|
||||
fmt.Printf("Name: %s\n", snapshot.Name)
|
||||
fmt.Printf("Source table: %s\n", snapshot.SourceTable)
|
||||
fmt.Printf("Created at: %s\n", snapshot.CreateTime.Format(timeLayout))
|
||||
fmt.Printf("Expires at: %s\n", snapshot.DeleteTime.Format(timeLayout))
|
||||
}
|
||||
|
||||
func doDeleteSnapshot(ctx context.Context, args ...string) {
|
||||
if len(args) != 2 {
|
||||
log.Fatal("usage: cbt deletesnapshot <cluster> <snapshot>")
|
||||
}
|
||||
cluster := args[0]
|
||||
snapshot := args[1]
|
||||
|
||||
err := getAdminClient().DeleteSnapshot(ctx, cluster, snapshot)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to delete snapshot: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+132
-7
@@ -17,7 +17,9 @@
|
||||
//go:generate go run cbt.go -o cbtdoc.go doc
|
||||
|
||||
/*
|
||||
Cbt is a tool for doing basic interactions with Cloud Bigtable.
|
||||
Cbt is a tool for doing basic interactions with Cloud Bigtable. To learn how to
|
||||
install the cbt tool, see the
|
||||
[cbt overview](https://cloud.google.com/bigtable/docs/go/cbt-overview).
|
||||
|
||||
Usage:
|
||||
|
||||
@@ -26,33 +28,58 @@ Usage:
|
||||
The commands are:
|
||||
|
||||
count Count rows in a table
|
||||
createinstance Create an instance with an initial cluster
|
||||
createcluster Create a cluster in the configured instance (replication alpha)
|
||||
createfamily Create a column family
|
||||
createtable Create a table
|
||||
updatecluster Update a cluster in the configured instance
|
||||
deleteinstance Deletes an instance
|
||||
deletecluster Deletes a cluster from the configured instance (replication alpha)
|
||||
deletecolumn Delete all cells in a column
|
||||
deletefamily Delete a column family
|
||||
deleterow Delete a row
|
||||
deletetable Delete a table
|
||||
doc Print godoc-suitable documentation for cbt
|
||||
help Print help text
|
||||
listinstances List instances in a project
|
||||
listclusters List instances in an instance
|
||||
lookup Read from a single row
|
||||
ls List tables and column families
|
||||
mddoc Print documentation for cbt in Markdown format
|
||||
read Read rows
|
||||
set Set value of a cell
|
||||
setgcpolicy Set the GC policy for a column family
|
||||
waitforreplication Blocks until all the completed writes have been replicated to all the clusters (replication alpha)
|
||||
version Print the current cbt version
|
||||
|
||||
Use "cbt help <command>" for more information about a command.
|
||||
|
||||
The options are:
|
||||
|
||||
-project string
|
||||
project ID
|
||||
project ID, if unset uses gcloud configured project
|
||||
-instance string
|
||||
Cloud Bigtable instance
|
||||
-creds string
|
||||
if set, use application credentials in this file
|
||||
|
||||
|
||||
Alpha features are not currently available to most Cloud Bigtable customers. The
|
||||
features might be changed in backward-incompatible ways and are not recommended
|
||||
for production use. They are not subject to any SLA or deprecation policy.
|
||||
|
||||
For convenience, values of the -project, -instance, -creds,
|
||||
-admin-endpoint and -data-endpoint flags may be specified in
|
||||
/usr/local/google/home/igorbernstein/.cbtrc in this format:
|
||||
project = my-project-123
|
||||
instance = my-instance
|
||||
creds = path-to-account-key.json
|
||||
admin-endpoint = hostname:port
|
||||
data-endpoint = hostname:port
|
||||
All values are optional, and all will be overridden by flags.
|
||||
|
||||
|
||||
|
||||
Count rows in a table
|
||||
|
||||
Usage:
|
||||
@@ -61,6 +88,34 @@ Usage:
|
||||
|
||||
|
||||
|
||||
Create an instance with an initial cluster
|
||||
|
||||
Usage:
|
||||
cbt createinstance <instance-id> <display-name> <cluster-id> <zone> <num-nodes> <storage type>
|
||||
instance-id Permanent, unique id for the instance
|
||||
display-name Description of the instance
|
||||
cluster-id Permanent, unique id for the cluster in the instance
|
||||
zone The zone in which to create the cluster
|
||||
num-nodes The number of nodes to create
|
||||
storage-type SSD or HDD
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Create a cluster in the configured instance (replication alpha)
|
||||
|
||||
Usage:
|
||||
cbt createcluster <cluster-id> <zone> <num-nodes> <storage type>
|
||||
cluster-id Permanent, unique id for the cluster in the instance
|
||||
zone The zone in which to create the cluster
|
||||
num-nodes The number of nodes to create
|
||||
storage-type SSD or HDD
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Create a column family
|
||||
|
||||
Usage:
|
||||
@@ -72,7 +127,46 @@ Usage:
|
||||
Create a table
|
||||
|
||||
Usage:
|
||||
cbt createtable <table>
|
||||
cbt createtable <table> [families=family[:(maxage=<d> | maxversions=<n>)],...] [splits=split,...]
|
||||
families: Column families and their associated GC policies. See "setgcpolicy".
|
||||
Example: families=family1:maxage=1w,family2:maxversions=1
|
||||
splits: Row key to be used to initially split the table
|
||||
|
||||
|
||||
|
||||
|
||||
Update a cluster in the configured instance
|
||||
|
||||
Usage:
|
||||
cbt updatecluster <cluster-id> [num-nodes=num-nodes]
|
||||
cluster-id Permanent, unique id for the cluster in the instance
|
||||
num-nodes The number of nodes to update to
|
||||
|
||||
|
||||
|
||||
|
||||
Deletes an instance
|
||||
|
||||
Usage:
|
||||
cbt deleteinstance <instance>
|
||||
|
||||
|
||||
|
||||
|
||||
Deletes a cluster from the configured instance (replication alpha)
|
||||
|
||||
Usage:
|
||||
cbt deletecluster <cluster>
|
||||
|
||||
|
||||
|
||||
|
||||
Delete all cells in a column
|
||||
|
||||
Usage:
|
||||
cbt deletecolumn <table> <row> <family> <column> [app-profile=<app profile id>]
|
||||
app-profile=<app profile id> The app profile id to use for the request (replication alpha)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -88,7 +182,9 @@ Usage:
|
||||
Delete a row
|
||||
|
||||
Usage:
|
||||
cbt deleterow <table> <row>
|
||||
cbt deleterow <table> <row> [app-profile=<app profile id>]
|
||||
app-profile=<app profile id> The app profile id to use for the request (replication alpha)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -125,10 +221,20 @@ Usage:
|
||||
|
||||
|
||||
|
||||
List instances in an instance
|
||||
|
||||
Usage:
|
||||
cbt listclusters
|
||||
|
||||
|
||||
|
||||
|
||||
Read from a single row
|
||||
|
||||
Usage:
|
||||
cbt lookup <table> <row>
|
||||
cbt lookup <table> <row> [app-profile=<app profile id>]
|
||||
app-profile=<app profile id> The app profile id to use for the request (replication alpha)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -153,11 +259,13 @@ Usage:
|
||||
Read rows
|
||||
|
||||
Usage:
|
||||
cbt read <table> [start=<row>] [end=<row>] [prefix=<prefix>] [count=<n>]
|
||||
cbt read <table> [start=<row>] [end=<row>] [prefix=<prefix>] [regex=<regex>] [count=<n>] [app-profile=<app profile id>]
|
||||
start=<row> Start reading at this row
|
||||
end=<row> Stop reading before this row
|
||||
prefix=<prefix> Read rows with this prefix
|
||||
regex=<regex> Read rows with keys matching this regex
|
||||
count=<n> Read only this many rows
|
||||
app-profile=<app profile id> The app profile id to use for the request (replication alpha)
|
||||
|
||||
|
||||
|
||||
@@ -166,7 +274,8 @@ Usage:
|
||||
Set value of a cell
|
||||
|
||||
Usage:
|
||||
cbt set <table> <row> family:column=val[@ts] ...
|
||||
cbt set <table> <row> [app-profile=<app profile id>] family:column=val[@ts] ...
|
||||
app-profile=<app profile id> The app profile id to use for the request (replication alpha)
|
||||
family:column=val[@ts] may be repeated to set multiple cells.
|
||||
|
||||
ts is an optional integer timestamp.
|
||||
@@ -187,5 +296,21 @@ Usage:
|
||||
|
||||
|
||||
|
||||
Blocks until all the completed writes have been replicated to all the clusters (replication alpha)
|
||||
|
||||
Usage:
|
||||
cbt waitforreplication <table>
|
||||
|
||||
|
||||
|
||||
|
||||
Print the current cbt version
|
||||
|
||||
Usage:
|
||||
cbt version
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
package main
|
||||
|
||||
+5
-4
@@ -107,11 +107,12 @@ func main() {
|
||||
|
||||
// Create a scratch table.
|
||||
log.Printf("Setting up scratch table...")
|
||||
if err := adminClient.CreateTable(context.Background(), *scratchTable); err != nil {
|
||||
log.Fatalf("Making scratch table %q: %v", *scratchTable, err)
|
||||
tblConf := bigtable.TableConf{
|
||||
TableID: *scratchTable,
|
||||
Families: map[string]bigtable.GCPolicy{"f": bigtable.MaxVersionsPolicy(1)},
|
||||
}
|
||||
if err := adminClient.CreateColumnFamily(context.Background(), *scratchTable, "f"); err != nil {
|
||||
log.Fatalf("Making scratch table column family: %v", err)
|
||||
if err := adminClient.CreateTableFromConf(context.Background(), &tblConf); err != nil {
|
||||
log.Fatalf("Making scratch table %q: %v", *scratchTable, err)
|
||||
}
|
||||
// Upon a successful run, delete the table. Don't bother checking for errors.
|
||||
defer adminClient.DeleteTable(context.Background(), *scratchTable)
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ type EmulatedEnv struct {
|
||||
|
||||
// NewEmulatedEnv builds and starts the emulator based environment
|
||||
func NewEmulatedEnv(config IntegrationTestConfig) (*EmulatedEnv, error) {
|
||||
srv, err := bttest.NewServer("127.0.0.1:0", grpc.MaxRecvMsgSize(200<<20), grpc.MaxSendMsgSize(100<<20))
|
||||
srv, err := bttest.NewServer("localhost:0", grpc.MaxRecvMsgSize(200<<20), grpc.MaxSendMsgSize(100<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ type timestampRangeFilter struct {
|
||||
}
|
||||
|
||||
func (trf timestampRangeFilter) String() string {
|
||||
return fmt.Sprintf("timestamp_range(%s,%s)", trf.startTime, trf.endTime)
|
||||
return fmt.Sprintf("timestamp_range(%v,%v)", trf.startTime, trf.endTime)
|
||||
}
|
||||
|
||||
func (trf timestampRangeFilter) proto() *btpb.RowFilter {
|
||||
|
||||
+18
-18
@@ -135,24 +135,24 @@ func GCRuleToString(rule *bttdpb.GcRule) string {
|
||||
if rule == nil {
|
||||
return "<default>"
|
||||
}
|
||||
var ruleStr string
|
||||
if r, ok := rule.Rule.(*bttdpb.GcRule_MaxNumVersions); ok {
|
||||
ruleStr += MaxVersionsPolicy(int(r.MaxNumVersions)).String()
|
||||
} else if r, ok := rule.Rule.(*bttdpb.GcRule_MaxAge); ok {
|
||||
ruleStr += MaxAgePolicy(time.Duration(r.MaxAge.Seconds) * time.Second).String()
|
||||
} else if r, ok := rule.Rule.(*bttdpb.GcRule_Intersection_); ok {
|
||||
var chunks []string
|
||||
for _, intRule := range r.Intersection.Rules {
|
||||
chunks = append(chunks, GCRuleToString(intRule))
|
||||
}
|
||||
ruleStr += "(" + strings.Join(chunks, " && ") + ")"
|
||||
} else if r, ok := rule.Rule.(*bttdpb.GcRule_Union_); ok {
|
||||
var chunks []string
|
||||
for _, unionRule := range r.Union.Rules {
|
||||
chunks = append(chunks, GCRuleToString(unionRule))
|
||||
}
|
||||
ruleStr += "(" + strings.Join(chunks, " || ") + ")"
|
||||
switch r := rule.Rule.(type) {
|
||||
case *bttdpb.GcRule_MaxNumVersions:
|
||||
return MaxVersionsPolicy(int(r.MaxNumVersions)).String()
|
||||
case *bttdpb.GcRule_MaxAge:
|
||||
return MaxAgePolicy(time.Duration(r.MaxAge.Seconds) * time.Second).String()
|
||||
case *bttdpb.GcRule_Intersection_:
|
||||
return joinRules(r.Intersection.Rules, " && ")
|
||||
case *bttdpb.GcRule_Union_:
|
||||
return joinRules(r.Union.Rules, " || ")
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
return ruleStr
|
||||
func joinRules(rules []*bttdpb.GcRule, sep string) string {
|
||||
var chunks []string
|
||||
for _, r := range rules {
|
||||
chunks = append(chunks, GCRuleToString(r))
|
||||
}
|
||||
return "(" + strings.Join(chunks, sep) + ")"
|
||||
}
|
||||
|
||||
+12
-11
@@ -20,10 +20,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
btspb "google.golang.org/genproto/googleapis/bigtable/v2"
|
||||
@@ -48,7 +49,7 @@ func TestSingleCell(t *testing.T) {
|
||||
t.Fatalf("Family name length mismatch %d, %d", 1, len(row["fm"]))
|
||||
}
|
||||
want := []ReadItem{ri("rk", "fm", "col", 1, "value")}
|
||||
if !reflect.DeepEqual(row["fm"], want) {
|
||||
if !testutil.Equal(row["fm"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm"], want)
|
||||
}
|
||||
if err := cr.Close(); err != nil {
|
||||
@@ -76,14 +77,14 @@ func TestMultipleCells(t *testing.T) {
|
||||
ri("rs", "fm1", "col1", 1, "val2"),
|
||||
ri("rs", "fm1", "col2", 0, "val3"),
|
||||
}
|
||||
if !reflect.DeepEqual(row["fm1"], want) {
|
||||
if !testutil.Equal(row["fm1"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm1"], want)
|
||||
}
|
||||
want = []ReadItem{
|
||||
ri("rs", "fm2", "col1", 0, "val4"),
|
||||
ri("rs", "fm2", "col2", 1, "extralongval5"),
|
||||
}
|
||||
if !reflect.DeepEqual(row["fm2"], want) {
|
||||
if !testutil.Equal(row["fm2"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm2"], want)
|
||||
}
|
||||
if err := cr.Close(); err != nil {
|
||||
@@ -108,7 +109,7 @@ func TestSplitCells(t *testing.T) {
|
||||
ri("rs", "fm1", "col1", 0, "hello world"),
|
||||
ri("rs", "fm1", "col2", 0, "val2"),
|
||||
}
|
||||
if !reflect.DeepEqual(row["fm1"], want) {
|
||||
if !testutil.Equal(row["fm1"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm1"], want)
|
||||
}
|
||||
if err := cr.Close(); err != nil {
|
||||
@@ -124,7 +125,7 @@ func TestMultipleRows(t *testing.T) {
|
||||
t.Fatalf("Processing chunk: %v", err)
|
||||
}
|
||||
want := []ReadItem{ri("rs1", "fm1", "col1", 1, "val1")}
|
||||
if !reflect.DeepEqual(row["fm1"], want) {
|
||||
if !testutil.Equal(row["fm1"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm1"], want)
|
||||
}
|
||||
|
||||
@@ -133,7 +134,7 @@ func TestMultipleRows(t *testing.T) {
|
||||
t.Fatalf("Processing chunk: %v", err)
|
||||
}
|
||||
want = []ReadItem{ri("rs2", "fm2", "col2", 2, "val2")}
|
||||
if !reflect.DeepEqual(row["fm2"], want) {
|
||||
if !testutil.Equal(row["fm2"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm2"], want)
|
||||
}
|
||||
|
||||
@@ -150,7 +151,7 @@ func TestBlankQualifier(t *testing.T) {
|
||||
t.Fatalf("Processing chunk: %v", err)
|
||||
}
|
||||
want := []ReadItem{ri("rs1", "fm1", "", 1, "val1")}
|
||||
if !reflect.DeepEqual(row["fm1"], want) {
|
||||
if !testutil.Equal(row["fm1"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm1"], want)
|
||||
}
|
||||
|
||||
@@ -159,7 +160,7 @@ func TestBlankQualifier(t *testing.T) {
|
||||
t.Fatalf("Processing chunk: %v", err)
|
||||
}
|
||||
want = []ReadItem{ri("rs2", "fm2", "col2", 2, "val2")}
|
||||
if !reflect.DeepEqual(row["fm2"], want) {
|
||||
if !testutil.Equal(row["fm2"], want) {
|
||||
t.Fatalf("Incorrect ReadItem: got: %v\nwant: %v\n", row["fm2"], want)
|
||||
}
|
||||
|
||||
@@ -177,7 +178,7 @@ func TestReset(t *testing.T) {
|
||||
cr.Process(ccReset())
|
||||
row, _ := cr.Process(cc("rs1", "fm1", "col1", 1, "val1", 0, true))
|
||||
want := []ReadItem{ri("rs1", "fm1", "col1", 1, "val1")}
|
||||
if !reflect.DeepEqual(row["fm1"], want) {
|
||||
if !testutil.Equal(row["fm1"], want) {
|
||||
t.Fatalf("Reset: got: %v\nwant: %v\n", row["fm1"], want)
|
||||
}
|
||||
if err := cr.Close(); err != nil {
|
||||
@@ -279,7 +280,7 @@ func runTestCase(t *testing.T, test TestCase) {
|
||||
|
||||
got := toSet(results)
|
||||
want := toSet(test.Results)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Fatalf("[%s]: got: %v\nwant: %v\n", test.Name, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
+9
-8
@@ -16,13 +16,14 @@ limitations under the License.
|
||||
package bigtable
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/bigtable/bttest"
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"github.com/golang/protobuf/ptypes/wrappers"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
btpb "google.golang.org/genproto/googleapis/bigtable/v2"
|
||||
@@ -32,7 +33,7 @@ import (
|
||||
)
|
||||
|
||||
func setupFakeServer(opt ...grpc.ServerOption) (tbl *Table, cleanup func(), err error) {
|
||||
srv, err := bttest.NewServer("127.0.0.1:0", opt...)
|
||||
srv, err := bttest.NewServer("localhost:0", opt...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -231,13 +232,13 @@ func TestRetryApplyBulk(t *testing.T) {
|
||||
}
|
||||
errors, err = tbl.ApplyBulk(ctx, []string{"row1", "row2"}, []*Mutation{m1, niMut})
|
||||
if err != nil {
|
||||
t.Errorf("unretryable errors: request failed %v")
|
||||
t.Errorf("unretryable errors: request failed %v", err)
|
||||
}
|
||||
want := []error{
|
||||
grpc.Errorf(codes.FailedPrecondition, ""),
|
||||
grpc.Errorf(codes.Aborted, ""),
|
||||
}
|
||||
if !reflect.DeepEqual(want, errors) {
|
||||
if !testutil.Equal(want, errors) {
|
||||
t.Errorf("unretryable errors: got: %v, want: %v", errors, want)
|
||||
}
|
||||
|
||||
@@ -273,7 +274,7 @@ func TestRetainRowsAfter(t *testing.T) {
|
||||
prevRowKey := "m"
|
||||
want := NewRange("m\x00", "z")
|
||||
got := prevRowRange.retainRowsAfter(prevRowKey)
|
||||
if !reflect.DeepEqual(want, got) {
|
||||
if !testutil.Equal(want, got, cmp.AllowUnexported(RowRange{})) {
|
||||
t.Errorf("range retry: got %v, want %v", got, want)
|
||||
}
|
||||
|
||||
@@ -281,7 +282,7 @@ func TestRetainRowsAfter(t *testing.T) {
|
||||
prevRowKey = "f"
|
||||
wantRowRangeList := RowRangeList{NewRange("f\x00", "g"), NewRange("h", "l")}
|
||||
got = prevRowRangeList.retainRowsAfter(prevRowKey)
|
||||
if !reflect.DeepEqual(wantRowRangeList, got) {
|
||||
if !testutil.Equal(wantRowRangeList, got, cmp.AllowUnexported(RowRange{})) {
|
||||
t.Errorf("range list retry: got %v, want %v", got, wantRowRangeList)
|
||||
}
|
||||
|
||||
@@ -289,7 +290,7 @@ func TestRetainRowsAfter(t *testing.T) {
|
||||
prevRowKey = "b"
|
||||
wantList := RowList{"c", "d", "e", "f"}
|
||||
got = prevRowList.retainRowsAfter(prevRowKey)
|
||||
if !reflect.DeepEqual(wantList, got) {
|
||||
if !testutil.Equal(wantList, got) {
|
||||
t.Errorf("list retry: got %v, want %v", got, wantList)
|
||||
}
|
||||
}
|
||||
@@ -351,7 +352,7 @@ func TestRetryReadRows(t *testing.T) {
|
||||
return true
|
||||
})
|
||||
want := []string{"a", "b", "c", "d"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Errorf("retry range integration: got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user