+67
@@ -0,0 +1,67 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
var _ = fmt.Sprintf
|
||||
var _ = iterator.Done
|
||||
var _ = strconv.FormatUint
|
||||
var _ = time.Now
|
||||
|
||||
func TestPublisherSmoke(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping smoke test in short mode")
|
||||
}
|
||||
ctx := context.Background()
|
||||
ts := testutil.TokenSource(ctx, DefaultAuthScopes()...)
|
||||
if ts == nil {
|
||||
t.Skip("Integration tests skipped. See CONTRIBUTING.md for details")
|
||||
}
|
||||
|
||||
projectId := testutil.ProjID()
|
||||
_ = projectId
|
||||
|
||||
c, err := NewPublisherClient(ctx, option.WithTokenSource(ts))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var formattedProject string = fmt.Sprintf("projects/%s", projectId)
|
||||
var request = &pubsubpb.ListTopicsRequest{
|
||||
Project: formattedProject,
|
||||
}
|
||||
|
||||
iter := c.ListTopics(ctx, request)
|
||||
if _, err := iter.Next(); err != nil && err != iterator.Done {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
Auto-generated pubsub v1 clients
|
||||
=================================
|
||||
|
||||
This package includes auto-generated clients for the pubsub v1 API.
|
||||
|
||||
Use the handwritten client (in the parent directory,
|
||||
cloud.google.com/go/pubsub) in preference to this.
|
||||
|
||||
This code is EXPERIMENTAL and subject to CHANGE AT ANY TIME.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
// Package pubsub is an auto-generated package for the
|
||||
// Google Cloud Pub/Sub API.
|
||||
//
|
||||
// NOTE: This package is in alpha. It is not stable, and is likely to change.
|
||||
//
|
||||
// Provides reliable, many-to-many, asynchronous messaging between
|
||||
// applications.
|
||||
//
|
||||
// Use the client at cloud.google.com/go/pubsub in preference to this.
|
||||
package pubsub // import "cloud.google.com/go/pubsub/apiv1"
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
func insertMetadata(ctx context.Context, mds ...metadata.MD) context.Context {
|
||||
out, _ := metadata.FromOutgoingContext(ctx)
|
||||
out = out.Copy()
|
||||
for _, md := range mds {
|
||||
for k, v := range md {
|
||||
out[k] = append(out[k], v...)
|
||||
}
|
||||
}
|
||||
return metadata.NewOutgoingContext(ctx, out)
|
||||
}
|
||||
|
||||
// DefaultAuthScopes reports the default set of authentication scopes to use with this package.
|
||||
func DefaultAuthScopes() []string {
|
||||
return []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/pubsub",
|
||||
}
|
||||
}
|
||||
+1878
File diff suppressed because it is too large
Load Diff
+95
@@ -0,0 +1,95 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
// PublisherProjectPath returns the path for the project resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s", project)
|
||||
// instead.
|
||||
func PublisherProjectPath(project string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
""
|
||||
}
|
||||
|
||||
// PublisherTopicPath returns the path for the topic resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/topics/%s", project, topic)
|
||||
// instead.
|
||||
func PublisherTopicPath(project, topic string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/topics/" +
|
||||
topic +
|
||||
""
|
||||
}
|
||||
|
||||
// SubscriberProjectPath returns the path for the project resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s", project)
|
||||
// instead.
|
||||
func SubscriberProjectPath(project string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
""
|
||||
}
|
||||
|
||||
// SubscriberSnapshotPath returns the path for the snapshot resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/snapshots/%s", project, snapshot)
|
||||
// instead.
|
||||
func SubscriberSnapshotPath(project, snapshot string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/snapshots/" +
|
||||
snapshot +
|
||||
""
|
||||
}
|
||||
|
||||
// SubscriberSubscriptionPath returns the path for the subscription resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/subscriptions/%s", project, subscription)
|
||||
// instead.
|
||||
func SubscriberSubscriptionPath(project, subscription string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/subscriptions/" +
|
||||
subscription +
|
||||
""
|
||||
}
|
||||
|
||||
// SubscriberTopicPath returns the path for the topic resource.
|
||||
//
|
||||
// Deprecated: Use
|
||||
// fmt.Sprintf("projects/%s/topics/%s", project, topic)
|
||||
// instead.
|
||||
func SubscriberTopicPath(project, topic string) string {
|
||||
return "" +
|
||||
"projects/" +
|
||||
project +
|
||||
"/topics/" +
|
||||
topic +
|
||||
""
|
||||
}
|
||||
+398
@@ -0,0 +1,398 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/iam"
|
||||
"cloud.google.com/go/internal/version"
|
||||
gax "github.com/googleapis/gax-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/transport"
|
||||
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// PublisherCallOptions contains the retry settings for each method of PublisherClient.
|
||||
type PublisherCallOptions struct {
|
||||
CreateTopic []gax.CallOption
|
||||
UpdateTopic []gax.CallOption
|
||||
Publish []gax.CallOption
|
||||
GetTopic []gax.CallOption
|
||||
ListTopics []gax.CallOption
|
||||
ListTopicSubscriptions []gax.CallOption
|
||||
DeleteTopic []gax.CallOption
|
||||
}
|
||||
|
||||
func defaultPublisherClientOptions() []option.ClientOption {
|
||||
return []option.ClientOption{
|
||||
option.WithEndpoint("pubsub.googleapis.com:443"),
|
||||
option.WithScopes(DefaultAuthScopes()...),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPublisherCallOptions() *PublisherCallOptions {
|
||||
retry := map[[2]string][]gax.CallOption{
|
||||
{"default", "idempotent"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.DeadlineExceeded,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
{"messaging", "one_plus_delivery"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.Aborted,
|
||||
codes.Canceled,
|
||||
codes.DeadlineExceeded,
|
||||
codes.Internal,
|
||||
codes.ResourceExhausted,
|
||||
codes.Unavailable,
|
||||
codes.Unknown,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
}
|
||||
return &PublisherCallOptions{
|
||||
CreateTopic: retry[[2]string{"default", "idempotent"}],
|
||||
UpdateTopic: retry[[2]string{"default", "idempotent"}],
|
||||
Publish: retry[[2]string{"messaging", "one_plus_delivery"}],
|
||||
GetTopic: retry[[2]string{"default", "idempotent"}],
|
||||
ListTopics: retry[[2]string{"default", "idempotent"}],
|
||||
ListTopicSubscriptions: retry[[2]string{"default", "idempotent"}],
|
||||
DeleteTopic: retry[[2]string{"default", "idempotent"}],
|
||||
}
|
||||
}
|
||||
|
||||
// PublisherClient is a client for interacting with Google Cloud Pub/Sub API.
|
||||
type PublisherClient struct {
|
||||
// The connection to the service.
|
||||
conn *grpc.ClientConn
|
||||
|
||||
// The gRPC API client.
|
||||
publisherClient pubsubpb.PublisherClient
|
||||
|
||||
// The call options for this service.
|
||||
CallOptions *PublisherCallOptions
|
||||
|
||||
// The x-goog-* metadata to be sent with each request.
|
||||
xGoogMetadata metadata.MD
|
||||
}
|
||||
|
||||
// NewPublisherClient creates a new publisher client.
|
||||
//
|
||||
// The service that an application uses to manipulate topics, and to send
|
||||
// messages to a topic.
|
||||
func NewPublisherClient(ctx context.Context, opts ...option.ClientOption) (*PublisherClient, error) {
|
||||
conn, err := transport.DialGRPC(ctx, append(defaultPublisherClientOptions(), opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &PublisherClient{
|
||||
conn: conn,
|
||||
CallOptions: defaultPublisherCallOptions(),
|
||||
|
||||
publisherClient: pubsubpb.NewPublisherClient(conn),
|
||||
}
|
||||
c.SetGoogleClientInfo()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Connection returns the client's connection to the API service.
|
||||
func (c *PublisherClient) Connection() *grpc.ClientConn {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
// Close closes the connection to the API service. The user should invoke this when
|
||||
// the client is no longer required.
|
||||
func (c *PublisherClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// SetGoogleClientInfo sets the name and version of the application in
|
||||
// the `x-goog-api-client` header passed on each request. Intended for
|
||||
// use by Google-written clients.
|
||||
func (c *PublisherClient) SetGoogleClientInfo(keyval ...string) {
|
||||
kv := append([]string{"gl-go", version.Go()}, keyval...)
|
||||
kv = append(kv, "gapic", version.Repo, "gax", gax.Version, "grpc", grpc.Version)
|
||||
c.xGoogMetadata = metadata.Pairs("x-goog-api-client", gax.XGoogHeader(kv...))
|
||||
}
|
||||
|
||||
func (c *PublisherClient) SubscriptionIAM(subscription *pubsubpb.Subscription) *iam.Handle {
|
||||
return iam.InternalNewHandle(c.Connection(), subscription.Name)
|
||||
}
|
||||
|
||||
func (c *PublisherClient) TopicIAM(topic *pubsubpb.Topic) *iam.Handle {
|
||||
return iam.InternalNewHandle(c.Connection(), topic.Name)
|
||||
}
|
||||
|
||||
// CreateTopic creates the given topic with the given name.
|
||||
func (c *PublisherClient) CreateTopic(ctx context.Context, req *pubsubpb.Topic, opts ...gax.CallOption) (*pubsubpb.Topic, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.CreateTopic[0:len(c.CallOptions.CreateTopic):len(c.CallOptions.CreateTopic)], opts...)
|
||||
var resp *pubsubpb.Topic
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.publisherClient.CreateTopic(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UpdateTopic updates an existing topic. Note that certain properties of a topic are not
|
||||
// modifiable. Options settings follow the style guide:
|
||||
// NOTE: The style guide requires body: "topic" instead of body: "*".
|
||||
// Keeping the latter for internal consistency in V1, however it should be
|
||||
// corrected in V2. See
|
||||
// https://cloud.google.com/apis/design/standard_methods#update for details.
|
||||
func (c *PublisherClient) UpdateTopic(ctx context.Context, req *pubsubpb.UpdateTopicRequest, opts ...gax.CallOption) (*pubsubpb.Topic, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.UpdateTopic[0:len(c.CallOptions.UpdateTopic):len(c.CallOptions.UpdateTopic)], opts...)
|
||||
var resp *pubsubpb.Topic
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.publisherClient.UpdateTopic(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Publish adds one or more messages to the topic. Returns NOT_FOUND if the topic
|
||||
// does not exist. The message payload must not be empty; it must contain
|
||||
// either a non-empty data field, or at least one attribute.
|
||||
func (c *PublisherClient) Publish(ctx context.Context, req *pubsubpb.PublishRequest, opts ...gax.CallOption) (*pubsubpb.PublishResponse, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Publish[0:len(c.CallOptions.Publish):len(c.CallOptions.Publish)], opts...)
|
||||
var resp *pubsubpb.PublishResponse
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.publisherClient.Publish(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetTopic gets the configuration of a topic.
|
||||
func (c *PublisherClient) GetTopic(ctx context.Context, req *pubsubpb.GetTopicRequest, opts ...gax.CallOption) (*pubsubpb.Topic, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.GetTopic[0:len(c.CallOptions.GetTopic):len(c.CallOptions.GetTopic)], opts...)
|
||||
var resp *pubsubpb.Topic
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.publisherClient.GetTopic(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListTopics lists matching topics.
|
||||
func (c *PublisherClient) ListTopics(ctx context.Context, req *pubsubpb.ListTopicsRequest, opts ...gax.CallOption) *TopicIterator {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ListTopics[0:len(c.CallOptions.ListTopics):len(c.CallOptions.ListTopics)], opts...)
|
||||
it := &TopicIterator{}
|
||||
it.InternalFetch = func(pageSize int, pageToken string) ([]*pubsubpb.Topic, string, error) {
|
||||
var resp *pubsubpb.ListTopicsResponse
|
||||
req.PageToken = pageToken
|
||||
if pageSize > math.MaxInt32 {
|
||||
req.PageSize = math.MaxInt32
|
||||
} else {
|
||||
req.PageSize = int32(pageSize)
|
||||
}
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.publisherClient.ListTopics(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return resp.Topics, resp.NextPageToken, nil
|
||||
}
|
||||
fetch := func(pageSize int, pageToken string) (string, error) {
|
||||
items, nextPageToken, err := it.InternalFetch(pageSize, pageToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
it.items = append(it.items, items...)
|
||||
return nextPageToken, nil
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf)
|
||||
return it
|
||||
}
|
||||
|
||||
// ListTopicSubscriptions lists the name of the subscriptions for this topic.
|
||||
func (c *PublisherClient) ListTopicSubscriptions(ctx context.Context, req *pubsubpb.ListTopicSubscriptionsRequest, opts ...gax.CallOption) *StringIterator {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ListTopicSubscriptions[0:len(c.CallOptions.ListTopicSubscriptions):len(c.CallOptions.ListTopicSubscriptions)], opts...)
|
||||
it := &StringIterator{}
|
||||
it.InternalFetch = func(pageSize int, pageToken string) ([]string, string, error) {
|
||||
var resp *pubsubpb.ListTopicSubscriptionsResponse
|
||||
req.PageToken = pageToken
|
||||
if pageSize > math.MaxInt32 {
|
||||
req.PageSize = math.MaxInt32
|
||||
} else {
|
||||
req.PageSize = int32(pageSize)
|
||||
}
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.publisherClient.ListTopicSubscriptions(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return resp.Subscriptions, resp.NextPageToken, nil
|
||||
}
|
||||
fetch := func(pageSize int, pageToken string) (string, error) {
|
||||
items, nextPageToken, err := it.InternalFetch(pageSize, pageToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
it.items = append(it.items, items...)
|
||||
return nextPageToken, nil
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf)
|
||||
return it
|
||||
}
|
||||
|
||||
// DeleteTopic deletes the topic with the given name. Returns NOT_FOUND if the topic
|
||||
// does not exist. After a topic is deleted, a new topic may be created with
|
||||
// the same name; this is an entirely new topic with none of the old
|
||||
// configuration or subscriptions. Existing subscriptions to this topic are
|
||||
// not deleted, but their topic field is set to _deleted-topic_.
|
||||
func (c *PublisherClient) DeleteTopic(ctx context.Context, req *pubsubpb.DeleteTopicRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.DeleteTopic[0:len(c.CallOptions.DeleteTopic):len(c.CallOptions.DeleteTopic)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.publisherClient.DeleteTopic(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// StringIterator manages a stream of string.
|
||||
type StringIterator struct {
|
||||
items []string
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []string, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *StringIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *StringIterator) Next() (string, error) {
|
||||
var item string
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *StringIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *StringIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
|
||||
// TopicIterator manages a stream of *pubsubpb.Topic.
|
||||
type TopicIterator struct {
|
||||
items []*pubsubpb.Topic
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []*pubsubpb.Topic, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *TopicIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *TopicIterator) Next() (*pubsubpb.Topic, error) {
|
||||
var item *pubsubpb.Topic
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *TopicIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *TopicIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
+204
@@ -0,0 +1,204 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package pubsub_test
|
||||
|
||||
import (
|
||||
"cloud.google.com/go/pubsub/apiv1"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
func ExampleNewPublisherClient() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use client.
|
||||
_ = c
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_SubscriptionIAM() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
subscription := &pubsubpb.Subscription{}
|
||||
h := c.SubscriptionIAM(subscription)
|
||||
policy, err := h.Policy(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
//TODO: Use the IAM policy
|
||||
_ = policy
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_TopicIAM() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
topic := &pubsubpb.Topic{}
|
||||
h := c.TopicIAM(topic)
|
||||
policy, err := h.Policy(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
//TODO: Use the IAM policy
|
||||
_ = policy
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_CreateTopic() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.Topic{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.CreateTopic(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_UpdateTopic() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.UpdateTopicRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.UpdateTopic(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_Publish() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.PublishRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.Publish(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_GetTopic() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.GetTopicRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.GetTopic(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_ListTopics() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.ListTopicsRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
it := c.ListTopics(ctx, req)
|
||||
for {
|
||||
resp, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_ListTopicSubscriptions() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.ListTopicSubscriptionsRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
it := c.ListTopicSubscriptions(ctx, req)
|
||||
for {
|
||||
resp, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExamplePublisherClient_DeleteTopic() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewPublisherClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.DeleteTopicRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.DeleteTopic(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/pubsub/apiv1"
|
||||
"golang.org/x/net/context"
|
||||
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
func ExampleSubscriberClient_Pull_lengthyClientProcessing() {
|
||||
projectID := "some-project"
|
||||
subscriptionID := "some-subscription"
|
||||
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sub := fmt.Sprintf("projects/%s/subscriptions/%s", projectID, subscriptionID)
|
||||
// Be sure to tune the MaxMessages parameter per your project's needs, and accordingly
|
||||
// adjust the ack behavior below to batch acknowledgements.
|
||||
req := pubsubpb.PullRequest{
|
||||
Subscription: sub,
|
||||
MaxMessages: 1,
|
||||
}
|
||||
|
||||
fmt.Println("Listening..")
|
||||
|
||||
for {
|
||||
res, err := client.Pull(ctx, &req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// client.Pull returns an empty list if there are no messages available in the
|
||||
// backlog. We should skip processing steps when that happens.
|
||||
if len(res.ReceivedMessages) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var recvdAckIDs []string
|
||||
for _, m := range res.ReceivedMessages {
|
||||
recvdAckIDs = append(recvdAckIDs, m.AckId)
|
||||
}
|
||||
|
||||
var done = make(chan struct{})
|
||||
var delay = 0 * time.Second // Tick immediately upon reception
|
||||
var ackDeadline = 10 * time.Second
|
||||
|
||||
// Continuously notify the server that processing is still happening on this batch.
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-done:
|
||||
return
|
||||
case <-time.After(delay):
|
||||
err := client.ModifyAckDeadline(ctx, &pubsubpb.ModifyAckDeadlineRequest{
|
||||
Subscription: sub,
|
||||
AckIds: recvdAckIDs,
|
||||
AckDeadlineSeconds: int32(ackDeadline.Seconds()),
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
delay = ackDeadline - 5*time.Second // 5 seconds grace period.
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for _, m := range res.ReceivedMessages {
|
||||
// Process the message here, possibly in a goroutine.
|
||||
log.Printf("Got message: %s", string(m.Message.Data))
|
||||
|
||||
err := client.Acknowledge(ctx, &pubsubpb.AcknowledgeRequest{
|
||||
Subscription: sub,
|
||||
AckIds: []string{m.AckId},
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
close(done)
|
||||
}
|
||||
}
|
||||
+593
@@ -0,0 +1,593 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/iam"
|
||||
"cloud.google.com/go/internal/version"
|
||||
gax "github.com/googleapis/gax-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/api/transport"
|
||||
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// SubscriberCallOptions contains the retry settings for each method of SubscriberClient.
|
||||
type SubscriberCallOptions struct {
|
||||
CreateSubscription []gax.CallOption
|
||||
GetSubscription []gax.CallOption
|
||||
UpdateSubscription []gax.CallOption
|
||||
ListSubscriptions []gax.CallOption
|
||||
DeleteSubscription []gax.CallOption
|
||||
ModifyAckDeadline []gax.CallOption
|
||||
Acknowledge []gax.CallOption
|
||||
Pull []gax.CallOption
|
||||
StreamingPull []gax.CallOption
|
||||
ModifyPushConfig []gax.CallOption
|
||||
ListSnapshots []gax.CallOption
|
||||
CreateSnapshot []gax.CallOption
|
||||
UpdateSnapshot []gax.CallOption
|
||||
DeleteSnapshot []gax.CallOption
|
||||
Seek []gax.CallOption
|
||||
}
|
||||
|
||||
func defaultSubscriberClientOptions() []option.ClientOption {
|
||||
return []option.ClientOption{
|
||||
option.WithEndpoint("pubsub.googleapis.com:443"),
|
||||
option.WithScopes(DefaultAuthScopes()...),
|
||||
}
|
||||
}
|
||||
|
||||
func defaultSubscriberCallOptions() *SubscriberCallOptions {
|
||||
retry := map[[2]string][]gax.CallOption{
|
||||
{"default", "idempotent"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.DeadlineExceeded,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
{"messaging", "pull"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.Canceled,
|
||||
codes.DeadlineExceeded,
|
||||
codes.Internal,
|
||||
codes.ResourceExhausted,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
{"streaming_messaging", "pull"}: {
|
||||
gax.WithRetry(func() gax.Retryer {
|
||||
return gax.OnCodes([]codes.Code{
|
||||
codes.Canceled,
|
||||
codes.DeadlineExceeded,
|
||||
codes.Internal,
|
||||
codes.ResourceExhausted,
|
||||
codes.Unavailable,
|
||||
}, gax.Backoff{
|
||||
Initial: 100 * time.Millisecond,
|
||||
Max: 60000 * time.Millisecond,
|
||||
Multiplier: 1.3,
|
||||
})
|
||||
}),
|
||||
},
|
||||
}
|
||||
return &SubscriberCallOptions{
|
||||
CreateSubscription: retry[[2]string{"default", "idempotent"}],
|
||||
GetSubscription: retry[[2]string{"default", "idempotent"}],
|
||||
UpdateSubscription: retry[[2]string{"default", "idempotent"}],
|
||||
ListSubscriptions: retry[[2]string{"default", "idempotent"}],
|
||||
DeleteSubscription: retry[[2]string{"default", "idempotent"}],
|
||||
ModifyAckDeadline: retry[[2]string{"default", "non_idempotent"}],
|
||||
Acknowledge: retry[[2]string{"messaging", "non_idempotent"}],
|
||||
Pull: retry[[2]string{"messaging", "pull"}],
|
||||
StreamingPull: retry[[2]string{"streaming_messaging", "pull"}],
|
||||
ModifyPushConfig: retry[[2]string{"default", "non_idempotent"}],
|
||||
ListSnapshots: retry[[2]string{"default", "idempotent"}],
|
||||
CreateSnapshot: retry[[2]string{"default", "idempotent"}],
|
||||
UpdateSnapshot: retry[[2]string{"default", "idempotent"}],
|
||||
DeleteSnapshot: retry[[2]string{"default", "idempotent"}],
|
||||
Seek: retry[[2]string{"default", "non_idempotent"}],
|
||||
}
|
||||
}
|
||||
|
||||
// SubscriberClient is a client for interacting with Google Cloud Pub/Sub API.
|
||||
type SubscriberClient struct {
|
||||
// The connection to the service.
|
||||
conn *grpc.ClientConn
|
||||
|
||||
// The gRPC API client.
|
||||
subscriberClient pubsubpb.SubscriberClient
|
||||
|
||||
// The call options for this service.
|
||||
CallOptions *SubscriberCallOptions
|
||||
|
||||
// The x-goog-* metadata to be sent with each request.
|
||||
xGoogMetadata metadata.MD
|
||||
}
|
||||
|
||||
// NewSubscriberClient creates a new subscriber client.
|
||||
//
|
||||
// The service that an application uses to manipulate subscriptions and to
|
||||
// consume messages from a subscription via the Pull method.
|
||||
func NewSubscriberClient(ctx context.Context, opts ...option.ClientOption) (*SubscriberClient, error) {
|
||||
conn, err := transport.DialGRPC(ctx, append(defaultSubscriberClientOptions(), opts...)...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c := &SubscriberClient{
|
||||
conn: conn,
|
||||
CallOptions: defaultSubscriberCallOptions(),
|
||||
|
||||
subscriberClient: pubsubpb.NewSubscriberClient(conn),
|
||||
}
|
||||
c.SetGoogleClientInfo()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Connection returns the client's connection to the API service.
|
||||
func (c *SubscriberClient) Connection() *grpc.ClientConn {
|
||||
return c.conn
|
||||
}
|
||||
|
||||
// Close closes the connection to the API service. The user should invoke this when
|
||||
// the client is no longer required.
|
||||
func (c *SubscriberClient) Close() error {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
// SetGoogleClientInfo sets the name and version of the application in
|
||||
// the `x-goog-api-client` header passed on each request. Intended for
|
||||
// use by Google-written clients.
|
||||
func (c *SubscriberClient) SetGoogleClientInfo(keyval ...string) {
|
||||
kv := append([]string{"gl-go", version.Go()}, keyval...)
|
||||
kv = append(kv, "gapic", version.Repo, "gax", gax.Version, "grpc", grpc.Version)
|
||||
c.xGoogMetadata = metadata.Pairs("x-goog-api-client", gax.XGoogHeader(kv...))
|
||||
}
|
||||
|
||||
func (c *SubscriberClient) SubscriptionIAM(subscription *pubsubpb.Subscription) *iam.Handle {
|
||||
return iam.InternalNewHandle(c.Connection(), subscription.Name)
|
||||
}
|
||||
|
||||
func (c *SubscriberClient) TopicIAM(topic *pubsubpb.Topic) *iam.Handle {
|
||||
return iam.InternalNewHandle(c.Connection(), topic.Name)
|
||||
}
|
||||
|
||||
// CreateSubscription creates a subscription to a given topic.
|
||||
// If the subscription already exists, returns ALREADY_EXISTS.
|
||||
// If the corresponding topic doesn't exist, returns NOT_FOUND.
|
||||
//
|
||||
// If the name is not provided in the request, the server will assign a random
|
||||
// name for this subscription on the same project as the topic, conforming
|
||||
// to the
|
||||
// resource name format (at https://cloud.google.com/pubsub/docs/overview#names).
|
||||
// The generated name is populated in the returned Subscription object.
|
||||
// Note that for REST API requests, you must specify a name in the request.
|
||||
func (c *SubscriberClient) CreateSubscription(ctx context.Context, req *pubsubpb.Subscription, opts ...gax.CallOption) (*pubsubpb.Subscription, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.CreateSubscription[0:len(c.CallOptions.CreateSubscription):len(c.CallOptions.CreateSubscription)], opts...)
|
||||
var resp *pubsubpb.Subscription
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.CreateSubscription(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetSubscription gets the configuration details of a subscription.
|
||||
func (c *SubscriberClient) GetSubscription(ctx context.Context, req *pubsubpb.GetSubscriptionRequest, opts ...gax.CallOption) (*pubsubpb.Subscription, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.GetSubscription[0:len(c.CallOptions.GetSubscription):len(c.CallOptions.GetSubscription)], opts...)
|
||||
var resp *pubsubpb.Subscription
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.GetSubscription(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UpdateSubscription updates an existing subscription. Note that certain properties of a
|
||||
// subscription, such as its topic, are not modifiable.
|
||||
// NOTE: The style guide requires body: "subscription" instead of body: "*".
|
||||
// Keeping the latter for internal consistency in V1, however it should be
|
||||
// corrected in V2. See
|
||||
// https://cloud.google.com/apis/design/standard_methods#update for details.
|
||||
func (c *SubscriberClient) UpdateSubscription(ctx context.Context, req *pubsubpb.UpdateSubscriptionRequest, opts ...gax.CallOption) (*pubsubpb.Subscription, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.UpdateSubscription[0:len(c.CallOptions.UpdateSubscription):len(c.CallOptions.UpdateSubscription)], opts...)
|
||||
var resp *pubsubpb.Subscription
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.UpdateSubscription(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ListSubscriptions lists matching subscriptions.
|
||||
func (c *SubscriberClient) ListSubscriptions(ctx context.Context, req *pubsubpb.ListSubscriptionsRequest, opts ...gax.CallOption) *SubscriptionIterator {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ListSubscriptions[0:len(c.CallOptions.ListSubscriptions):len(c.CallOptions.ListSubscriptions)], opts...)
|
||||
it := &SubscriptionIterator{}
|
||||
it.InternalFetch = func(pageSize int, pageToken string) ([]*pubsubpb.Subscription, string, error) {
|
||||
var resp *pubsubpb.ListSubscriptionsResponse
|
||||
req.PageToken = pageToken
|
||||
if pageSize > math.MaxInt32 {
|
||||
req.PageSize = math.MaxInt32
|
||||
} else {
|
||||
req.PageSize = int32(pageSize)
|
||||
}
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.ListSubscriptions(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return resp.Subscriptions, resp.NextPageToken, nil
|
||||
}
|
||||
fetch := func(pageSize int, pageToken string) (string, error) {
|
||||
items, nextPageToken, err := it.InternalFetch(pageSize, pageToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
it.items = append(it.items, items...)
|
||||
return nextPageToken, nil
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf)
|
||||
return it
|
||||
}
|
||||
|
||||
// DeleteSubscription deletes an existing subscription. All messages retained in the subscription
|
||||
// are immediately dropped. Calls to Pull after deletion will return
|
||||
// NOT_FOUND. After a subscription is deleted, a new one may be created with
|
||||
// the same name, but the new one has no association with the old
|
||||
// subscription or its topic unless the same topic is specified.
|
||||
func (c *SubscriberClient) DeleteSubscription(ctx context.Context, req *pubsubpb.DeleteSubscriptionRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.DeleteSubscription[0:len(c.CallOptions.DeleteSubscription):len(c.CallOptions.DeleteSubscription)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.subscriberClient.DeleteSubscription(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// ModifyAckDeadline modifies the ack deadline for a specific message. This method is useful
|
||||
// to indicate that more time is needed to process a message by the
|
||||
// subscriber, or to make the message available for redelivery if the
|
||||
// processing was interrupted. Note that this does not modify the
|
||||
// subscription-level ackDeadlineSeconds used for subsequent messages.
|
||||
func (c *SubscriberClient) ModifyAckDeadline(ctx context.Context, req *pubsubpb.ModifyAckDeadlineRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ModifyAckDeadline[0:len(c.CallOptions.ModifyAckDeadline):len(c.CallOptions.ModifyAckDeadline)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.subscriberClient.ModifyAckDeadline(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Acknowledge acknowledges the messages associated with the ack_ids in the
|
||||
// AcknowledgeRequest. The Pub/Sub system can remove the relevant messages
|
||||
// from the subscription.
|
||||
//
|
||||
// Acknowledging a message whose ack deadline has expired may succeed,
|
||||
// but such a message may be redelivered later. Acknowledging a message more
|
||||
// than once will not result in an error.
|
||||
func (c *SubscriberClient) Acknowledge(ctx context.Context, req *pubsubpb.AcknowledgeRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Acknowledge[0:len(c.CallOptions.Acknowledge):len(c.CallOptions.Acknowledge)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.subscriberClient.Acknowledge(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Pull pulls messages from the server. Returns an empty list if there are no
|
||||
// messages available in the backlog. The server may return UNAVAILABLE if
|
||||
// there are too many concurrent pull requests pending for the given
|
||||
// subscription.
|
||||
func (c *SubscriberClient) Pull(ctx context.Context, req *pubsubpb.PullRequest, opts ...gax.CallOption) (*pubsubpb.PullResponse, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Pull[0:len(c.CallOptions.Pull):len(c.CallOptions.Pull)], opts...)
|
||||
var resp *pubsubpb.PullResponse
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.Pull(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// StreamingPull (EXPERIMENTAL) StreamingPull is an experimental feature. This RPC will
|
||||
// respond with UNIMPLEMENTED errors unless you have been invited to test
|
||||
// this feature. Contact cloud-pubsub@google.com with any questions.
|
||||
//
|
||||
// Establishes a stream with the server, which sends messages down to the
|
||||
// client. The client streams acknowledgements and ack deadline modifications
|
||||
// back to the server. The server will close the stream and return the status
|
||||
// on any error. The server may close the stream with status OK to reassign
|
||||
// server-side resources, in which case, the client should re-establish the
|
||||
// stream. UNAVAILABLE may also be returned in the case of a transient error
|
||||
// (e.g., a server restart). These should also be retried by the client. Flow
|
||||
// control can be achieved by configuring the underlying RPC channel.
|
||||
func (c *SubscriberClient) StreamingPull(ctx context.Context, opts ...gax.CallOption) (pubsubpb.Subscriber_StreamingPullClient, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.StreamingPull[0:len(c.CallOptions.StreamingPull):len(c.CallOptions.StreamingPull)], opts...)
|
||||
var resp pubsubpb.Subscriber_StreamingPullClient
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.StreamingPull(ctx, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// ModifyPushConfig modifies the PushConfig for a specified subscription.
|
||||
//
|
||||
// This may be used to change a push subscription to a pull one (signified by
|
||||
// an empty PushConfig) or vice versa, or change the endpoint URL and other
|
||||
// attributes of a push subscription. Messages will accumulate for delivery
|
||||
// continuously through the call regardless of changes to the PushConfig.
|
||||
func (c *SubscriberClient) ModifyPushConfig(ctx context.Context, req *pubsubpb.ModifyPushConfigRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ModifyPushConfig[0:len(c.CallOptions.ModifyPushConfig):len(c.CallOptions.ModifyPushConfig)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.subscriberClient.ModifyPushConfig(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// ListSnapshots lists the existing snapshots.
|
||||
func (c *SubscriberClient) ListSnapshots(ctx context.Context, req *pubsubpb.ListSnapshotsRequest, opts ...gax.CallOption) *SnapshotIterator {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.ListSnapshots[0:len(c.CallOptions.ListSnapshots):len(c.CallOptions.ListSnapshots)], opts...)
|
||||
it := &SnapshotIterator{}
|
||||
it.InternalFetch = func(pageSize int, pageToken string) ([]*pubsubpb.Snapshot, string, error) {
|
||||
var resp *pubsubpb.ListSnapshotsResponse
|
||||
req.PageToken = pageToken
|
||||
if pageSize > math.MaxInt32 {
|
||||
req.PageSize = math.MaxInt32
|
||||
} else {
|
||||
req.PageSize = int32(pageSize)
|
||||
}
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.ListSnapshots(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return resp.Snapshots, resp.NextPageToken, nil
|
||||
}
|
||||
fetch := func(pageSize int, pageToken string) (string, error) {
|
||||
items, nextPageToken, err := it.InternalFetch(pageSize, pageToken)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
it.items = append(it.items, items...)
|
||||
return nextPageToken, nil
|
||||
}
|
||||
it.pageInfo, it.nextFunc = iterator.NewPageInfo(fetch, it.bufLen, it.takeBuf)
|
||||
return it
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a snapshot from the requested subscription.
|
||||
// If the snapshot already exists, returns ALREADY_EXISTS.
|
||||
// If the requested subscription doesn't exist, returns NOT_FOUND.
|
||||
//
|
||||
// If the name is not provided in the request, the server will assign a random
|
||||
// name for this snapshot on the same project as the subscription, conforming
|
||||
// to the
|
||||
// resource name format (at https://cloud.google.com/pubsub/docs/overview#names).
|
||||
// The generated name is populated in the returned Snapshot object.
|
||||
// Note that for REST API requests, you must specify a name in the request.
|
||||
func (c *SubscriberClient) CreateSnapshot(ctx context.Context, req *pubsubpb.CreateSnapshotRequest, opts ...gax.CallOption) (*pubsubpb.Snapshot, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.CreateSnapshot[0:len(c.CallOptions.CreateSnapshot):len(c.CallOptions.CreateSnapshot)], opts...)
|
||||
var resp *pubsubpb.Snapshot
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.CreateSnapshot(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// UpdateSnapshot updates an existing snapshot. Note that certain properties of a snapshot
|
||||
// are not modifiable.
|
||||
// NOTE: The style guide requires body: "snapshot" instead of body: "*".
|
||||
// Keeping the latter for internal consistency in V1, however it should be
|
||||
// corrected in V2. See
|
||||
// https://cloud.google.com/apis/design/standard_methods#update for details.
|
||||
func (c *SubscriberClient) UpdateSnapshot(ctx context.Context, req *pubsubpb.UpdateSnapshotRequest, opts ...gax.CallOption) (*pubsubpb.Snapshot, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.UpdateSnapshot[0:len(c.CallOptions.UpdateSnapshot):len(c.CallOptions.UpdateSnapshot)], opts...)
|
||||
var resp *pubsubpb.Snapshot
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.UpdateSnapshot(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// DeleteSnapshot removes an existing snapshot. All messages retained in the snapshot
|
||||
// are immediately dropped. After a snapshot is deleted, a new one may be
|
||||
// created with the same name, but the new one has no association with the old
|
||||
// snapshot or its subscription, unless the same subscription is specified.
|
||||
func (c *SubscriberClient) DeleteSnapshot(ctx context.Context, req *pubsubpb.DeleteSnapshotRequest, opts ...gax.CallOption) error {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.DeleteSnapshot[0:len(c.CallOptions.DeleteSnapshot):len(c.CallOptions.DeleteSnapshot)], opts...)
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
_, err = c.subscriberClient.DeleteSnapshot(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
return err
|
||||
}
|
||||
|
||||
// Seek seeks an existing subscription to a point in time or to a given snapshot,
|
||||
// whichever is provided in the request.
|
||||
func (c *SubscriberClient) Seek(ctx context.Context, req *pubsubpb.SeekRequest, opts ...gax.CallOption) (*pubsubpb.SeekResponse, error) {
|
||||
ctx = insertMetadata(ctx, c.xGoogMetadata)
|
||||
opts = append(c.CallOptions.Seek[0:len(c.CallOptions.Seek):len(c.CallOptions.Seek)], opts...)
|
||||
var resp *pubsubpb.SeekResponse
|
||||
err := gax.Invoke(ctx, func(ctx context.Context, settings gax.CallSettings) error {
|
||||
var err error
|
||||
resp, err = c.subscriberClient.Seek(ctx, req, settings.GRPC...)
|
||||
return err
|
||||
}, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// SnapshotIterator manages a stream of *pubsubpb.Snapshot.
|
||||
type SnapshotIterator struct {
|
||||
items []*pubsubpb.Snapshot
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []*pubsubpb.Snapshot, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the 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 if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *SnapshotIterator) Next() (*pubsubpb.Snapshot, error) {
|
||||
var item *pubsubpb.Snapshot
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *SnapshotIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *SnapshotIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
|
||||
// SubscriptionIterator manages a stream of *pubsubpb.Subscription.
|
||||
type SubscriptionIterator struct {
|
||||
items []*pubsubpb.Subscription
|
||||
pageInfo *iterator.PageInfo
|
||||
nextFunc func() error
|
||||
|
||||
// InternalFetch is for use by the Google Cloud Libraries only.
|
||||
// It is not part of the stable interface of this package.
|
||||
//
|
||||
// InternalFetch returns results from a single call to the underlying RPC.
|
||||
// The number of results is no greater than pageSize.
|
||||
// If there are no more results, nextPageToken is empty and err is nil.
|
||||
InternalFetch func(pageSize int, pageToken string) (results []*pubsubpb.Subscription, nextPageToken string, err error)
|
||||
}
|
||||
|
||||
// PageInfo supports pagination. See the google.golang.org/api/iterator package for details.
|
||||
func (it *SubscriptionIterator) PageInfo() *iterator.PageInfo {
|
||||
return it.pageInfo
|
||||
}
|
||||
|
||||
// Next returns the next result. Its second return value is iterator.Done if there are no more
|
||||
// results. Once Next returns Done, all subsequent calls will return Done.
|
||||
func (it *SubscriptionIterator) Next() (*pubsubpb.Subscription, error) {
|
||||
var item *pubsubpb.Subscription
|
||||
if err := it.nextFunc(); err != nil {
|
||||
return item, err
|
||||
}
|
||||
item = it.items[0]
|
||||
it.items = it.items[1:]
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (it *SubscriptionIterator) bufLen() int {
|
||||
return len(it.items)
|
||||
}
|
||||
|
||||
func (it *SubscriptionIterator) takeBuf() interface{} {
|
||||
b := it.items
|
||||
it.items = nil
|
||||
return b
|
||||
}
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
// Copyright 2018 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// AUTO-GENERATED CODE. DO NOT EDIT.
|
||||
|
||||
package pubsub_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"cloud.google.com/go/pubsub/apiv1"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
func ExampleNewSubscriberClient() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use client.
|
||||
_ = c
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_SubscriptionIAM() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
subscription := &pubsubpb.Subscription{}
|
||||
h := c.SubscriptionIAM(subscription)
|
||||
policy, err := h.Policy(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
//TODO: Use the IAM policy
|
||||
_ = policy
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_TopicIAM() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
topic := &pubsubpb.Topic{}
|
||||
h := c.TopicIAM(topic)
|
||||
policy, err := h.Policy(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
//TODO: Use the IAM policy
|
||||
_ = policy
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_CreateSubscription() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.Subscription{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.CreateSubscription(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_GetSubscription() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.GetSubscriptionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.GetSubscription(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_UpdateSubscription() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.UpdateSubscriptionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.UpdateSubscription(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_ListSubscriptions() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.ListSubscriptionsRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
it := c.ListSubscriptions(ctx, req)
|
||||
for {
|
||||
resp, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_DeleteSubscription() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.DeleteSubscriptionRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.DeleteSubscription(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_ModifyAckDeadline() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.ModifyAckDeadlineRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.ModifyAckDeadline(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_Acknowledge() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.AcknowledgeRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.Acknowledge(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_Pull() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.PullRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.Pull(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_StreamingPull() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
stream, err := c.StreamingPull(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
go func() {
|
||||
reqs := []*pubsubpb.StreamingPullRequest{
|
||||
// TODO: Create requests.
|
||||
}
|
||||
for _, req := range reqs {
|
||||
if err := stream.Send(req); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
stream.CloseSend()
|
||||
}()
|
||||
for {
|
||||
resp, err := stream.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_ModifyPushConfig() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.ModifyPushConfigRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.ModifyPushConfig(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_ListSnapshots() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.ListSnapshotsRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
it := c.ListSnapshots(ctx, req)
|
||||
for {
|
||||
resp, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_CreateSnapshot() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.CreateSnapshotRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.CreateSnapshot(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_UpdateSnapshot() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.UpdateSnapshotRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.UpdateSnapshot(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_DeleteSnapshot() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.DeleteSnapshotRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
err = c.DeleteSnapshot(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscriberClient_Seek() {
|
||||
ctx := context.Background()
|
||||
c, err := pubsub.NewSubscriberClient(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
req := &pubsubpb.SeekRequest{
|
||||
// TODO: Fill request struct fields.
|
||||
}
|
||||
resp, err := c.Seek(ctx, req)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// TODO: Use resp.
|
||||
_ = resp
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/*
|
||||
Package pubsub provides an easy way to publish and receive Google Cloud Pub/Sub
|
||||
messages, hiding the the details of the underlying server RPCs. Google Cloud
|
||||
Pub/Sub is a many-to-many, asynchronous messaging system that decouples senders
|
||||
and receivers.
|
||||
|
||||
Note: This package is in beta. Some backwards-incompatible changes may occur.
|
||||
|
||||
More information about Google Cloud Pub/Sub is available at
|
||||
https://cloud.google.com/pubsub/docs
|
||||
|
||||
Publishing
|
||||
|
||||
Google Cloud Pub/Sub messages are published to topics. Topics may be created
|
||||
using the pubsub package like so:
|
||||
|
||||
topic, err := pubsubClient.CreateTopic(context.Background(), "topic-name")
|
||||
|
||||
Messages may then be published to a topic:
|
||||
|
||||
res := topic.Publish(ctx, &pubsub.Message{Data: []byte("payload")})
|
||||
|
||||
Publish queues the message for publishing and returns immediately. When enough
|
||||
messages have accumulated, or enough time has elapsed, the batch of messages is
|
||||
sent to the Pub/Sub service.
|
||||
|
||||
Publish returns a PublishResult, which behaves like a future: its Get method
|
||||
blocks until the message has been sent to the service.
|
||||
|
||||
The first time you call Publish on a topic, goroutines are started in the
|
||||
background. To clean up these goroutines, call Stop:
|
||||
|
||||
topic.Stop()
|
||||
|
||||
Receiving
|
||||
|
||||
To receive messages published to a topic, clients create subscriptions
|
||||
to the topic. There may be more than one subscription per topic; each message
|
||||
that is published to the topic will be delivered to all of its subscriptions.
|
||||
|
||||
Subsciptions may be created like so:
|
||||
|
||||
sub, err := pubsubClient.CreateSubscription(context.Background(), "sub-name",
|
||||
pubsub.SubscriptionConfig{Topic: topic})
|
||||
|
||||
Messages are then consumed from a subscription via callback.
|
||||
|
||||
err := sub.Receive(context.Background(), func(ctx context.Context, m *Message) {
|
||||
log.Printf("Got message: %s", m.Data)
|
||||
m.Ack()
|
||||
})
|
||||
if err != nil {
|
||||
// Handle error.
|
||||
}
|
||||
|
||||
The callback is invoked concurrently by multiple goroutines, maximizing
|
||||
throughput. To terminate a call to Receive, cancel its context.
|
||||
|
||||
Once client code has processed the message, it must call Message.Ack, otherwise
|
||||
the message will eventually be redelivered. As an optimization, if the client
|
||||
cannot or doesn't want to process the message, it can call Message.Nack to
|
||||
speed redelivery. For more information and configuration options, see
|
||||
"Deadlines" below.
|
||||
|
||||
Note: It is possible for Messages to be redelivered, even if Message.Ack has
|
||||
been called. Client code must be robust to multiple deliveries of messages.
|
||||
|
||||
Deadlines
|
||||
|
||||
The default pubsub deadlines are suitable for most use cases, but may be
|
||||
overridden. This section describes the tradeoffs that should be considered
|
||||
when overriding the defaults.
|
||||
|
||||
Behind the scenes, each message returned by the Pub/Sub server has an
|
||||
associated lease, known as an "ACK deadline".
|
||||
Unless a message is acknowledged within the ACK deadline, or the client requests that
|
||||
the ACK deadline be extended, the message will become elegible for redelivery.
|
||||
As a convenience, the pubsub package will automatically extend deadlines until
|
||||
either:
|
||||
* Message.Ack or Message.Nack is called, or
|
||||
* the "MaxExtension" period elapses from the time the message is fetched from the server.
|
||||
|
||||
The initial ACK deadline given to each messages defaults to 10 seconds, but may
|
||||
be overridden during subscription creation. Selecting an ACK deadline is a
|
||||
tradeoff between message redelivery latency and RPC volume. If the pubsub
|
||||
package fails to acknowledge or extend a message (e.g. due to unexpected
|
||||
termination of the process), a shorter ACK deadline will generally result in
|
||||
faster message redelivery by the Pub/Sub system. However, a short ACK deadline
|
||||
may also increase the number of deadline extension RPCs that the pubsub package
|
||||
sends to the server.
|
||||
|
||||
The default max extension period is DefaultReceiveSettings.MaxExtension, and can
|
||||
be overridden by setting Subscription.ReceiveSettings.MaxExtension. Selecting a
|
||||
max extension period is a tradeoff between the speed at which client code must
|
||||
process messages, and the redelivery delay if messages fail to be acknowledged
|
||||
(e.g. because client code neglects to do so). Using a large MaxExtension
|
||||
increases the available time for client code to process messages. However, if
|
||||
the client code neglects to call Message.Ack/Nack, a large MaxExtension will
|
||||
increase the delay before the message is redelivered.
|
||||
|
||||
Authentication
|
||||
|
||||
See examples of authorization and authentication at
|
||||
https://godoc.org/cloud.google.com/go#pkg-examples.
|
||||
|
||||
Slow Message Processing
|
||||
|
||||
For use cases where message processing exceeds 30 minutes, we recommend using
|
||||
the base client in a pull model, since long-lived streams are periodically killed
|
||||
by firewalls. See the example at https://godoc.org/cloud.google.com/go/pubsub/apiv1#example-SubscriberClient-Pull-LengthyClientProcessing
|
||||
*/
|
||||
package pubsub // import "cloud.google.com/go/pubsub"
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
const (
|
||||
timeout = time.Minute * 10
|
||||
ackDeadline = time.Second * 10
|
||||
nMessages = 1e4
|
||||
acceptableDupPercentage = .05
|
||||
numAcceptableDups = int(nMessages * acceptableDupPercentage / 100)
|
||||
)
|
||||
|
||||
// Buffer log messages to debug failures.
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
// TestEndToEnd pumps many messages into a topic and tests that they are all
|
||||
// delivered to each subscription for the topic. It also tests that messages
|
||||
// are not unexpectedly redelivered.
|
||||
func TestEndToEnd(t *testing.T) {
|
||||
t.Parallel()
|
||||
if testing.Short() {
|
||||
t.Skip("Integration tests skipped in short mode")
|
||||
}
|
||||
log.SetOutput(&logBuf)
|
||||
ctx := context.Background()
|
||||
ts := testutil.TokenSource(ctx, ScopePubSub, ScopeCloudPlatform)
|
||||
if ts == nil {
|
||||
t.Skip("Integration tests skipped. See CONTRIBUTING.md for details")
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
topicName := fmt.Sprintf("endtoend-%d", now.UnixNano())
|
||||
subPrefix := fmt.Sprintf("endtoend-%d", now.UnixNano())
|
||||
|
||||
client, err := NewClient(ctx, testutil.ProjID(), option.WithTokenSource(ts))
|
||||
if err != nil {
|
||||
t.Fatalf("Creating client error: %v", err)
|
||||
}
|
||||
|
||||
var topic *Topic
|
||||
if topic, err = client.CreateTopic(ctx, topicName); err != nil {
|
||||
t.Fatalf("CreateTopic error: %v", err)
|
||||
}
|
||||
defer topic.Delete(ctx)
|
||||
|
||||
// Two subscriptions to the same topic.
|
||||
var subs [2]*Subscription
|
||||
for i := 0; i < len(subs); i++ {
|
||||
subs[i], err = client.CreateSubscription(ctx, fmt.Sprintf("%s-%d", subPrefix, i), SubscriptionConfig{
|
||||
Topic: topic,
|
||||
AckDeadline: ackDeadline,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSub error: %v", err)
|
||||
}
|
||||
defer subs[i].Delete(ctx)
|
||||
}
|
||||
|
||||
err = publish(ctx, topic, nMessages)
|
||||
topic.Stop()
|
||||
if err != nil {
|
||||
t.Fatalf("publish: %v", err)
|
||||
}
|
||||
|
||||
// recv provides an indication that messages are still arriving.
|
||||
recv := make(chan struct{})
|
||||
// We have two subscriptions to our topic.
|
||||
// Each subscription will get a copy of each published message.
|
||||
var wg sync.WaitGroup
|
||||
cctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
consumers := []*consumer{
|
||||
{counts: make(map[string]int), recv: recv, durations: []time.Duration{time.Hour}},
|
||||
{counts: make(map[string]int), recv: recv,
|
||||
durations: []time.Duration{ackDeadline, ackDeadline, ackDeadline / 2, ackDeadline / 2, time.Hour}},
|
||||
}
|
||||
for i, con := range consumers {
|
||||
con := con
|
||||
sub := subs[i]
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
con.consume(t, cctx, sub)
|
||||
}()
|
||||
}
|
||||
// Wait for a while after the last message before declaring quiescence.
|
||||
// We wait a multiple of the ack deadline, for two reasons:
|
||||
// 1. To detect if messages are redelivered after having their ack
|
||||
// deadline extended.
|
||||
// 2. To wait for redelivery of messages that were en route when a Receive
|
||||
// is canceled. This can take considerably longer than the ack deadline.
|
||||
quiescenceDur := ackDeadline * 6
|
||||
quiescenceTimer := time.NewTimer(quiescenceDur)
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-recv:
|
||||
// Reset timer so we wait quiescenceDur after the last message.
|
||||
// See https://godoc.org/time#Timer.Reset for why the Stop
|
||||
// and channel drain are necessary.
|
||||
if !quiescenceTimer.Stop() {
|
||||
<-quiescenceTimer.C
|
||||
}
|
||||
quiescenceTimer.Reset(quiescenceDur)
|
||||
|
||||
case <-quiescenceTimer.C:
|
||||
cancel()
|
||||
log.Println("quiesced")
|
||||
break loop
|
||||
|
||||
case <-cctx.Done():
|
||||
t.Fatal("timed out")
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
ok := true
|
||||
for i, con := range consumers {
|
||||
var numDups int
|
||||
var zeroes int
|
||||
for _, v := range con.counts {
|
||||
if v == 0 {
|
||||
zeroes += 1
|
||||
}
|
||||
numDups += v - 1
|
||||
}
|
||||
|
||||
if zeroes > 0 {
|
||||
t.Errorf("Consumer %d: %d messages never arrived", i, zeroes)
|
||||
ok = false
|
||||
} else if numDups > numAcceptableDups {
|
||||
t.Errorf("Consumer %d: Willing to accept %d dups (%f%% duplicated of %d messages), but got %d", i, numAcceptableDups, acceptableDupPercentage, int(nMessages), numDups)
|
||||
ok = false
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
logBuf.WriteTo(os.Stdout)
|
||||
}
|
||||
}
|
||||
|
||||
// publish publishes n messages to topic, and returns the published message IDs.
|
||||
func publish(ctx context.Context, topic *Topic, n int) error {
|
||||
var rs []*PublishResult
|
||||
for i := 0; i < n; i++ {
|
||||
m := &Message{Data: []byte(fmt.Sprintf("msg %d", i))}
|
||||
rs = append(rs, topic.Publish(ctx, m))
|
||||
}
|
||||
var ids []string
|
||||
for _, r := range rs {
|
||||
id, err := r.Get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// consumer consumes messages according to its configuration.
|
||||
type consumer struct {
|
||||
durations []time.Duration
|
||||
|
||||
// A value is sent to recv each time Inc is called.
|
||||
recv chan struct{}
|
||||
|
||||
mu sync.Mutex
|
||||
counts map[string]int
|
||||
total int
|
||||
}
|
||||
|
||||
// consume reads messages from a subscription, and keeps track of what it receives in mc.
|
||||
// After consume returns, the caller should wait on wg to ensure that no more updates to mc will be made.
|
||||
func (c *consumer) consume(t *testing.T, ctx context.Context, sub *Subscription) {
|
||||
for _, dur := range c.durations {
|
||||
ctx2, cancel := context.WithTimeout(ctx, dur)
|
||||
defer cancel()
|
||||
id := sub.name[len(sub.name)-2:]
|
||||
log.Printf("%s: start receive", id)
|
||||
prev := c.total
|
||||
err := sub.Receive(ctx2, c.process)
|
||||
log.Printf("%s: end receive; read %d", id, c.total-prev)
|
||||
if err != nil {
|
||||
t.Errorf("error from Receive: %v", err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// process handles a message and records it in mc.
|
||||
func (c *consumer) process(_ context.Context, m *Message) {
|
||||
c.mu.Lock()
|
||||
c.counts[m.ID] += 1
|
||||
c.total++
|
||||
c.mu.Unlock()
|
||||
c.recv <- struct{}{}
|
||||
// Simulate time taken to process m, while continuing to process more messages.
|
||||
// Some messages will need to have their ack deadline extended due to this delay.
|
||||
delay := rand.Intn(int(ackDeadline * 3))
|
||||
time.AfterFunc(time.Duration(delay), m.Ack)
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/pubsub"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
func ExampleClient_Subscriptions() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// List all subscriptions of the project.
|
||||
it := client.Subscriptions(ctx)
|
||||
_ = it // TODO: iterate using Next.
|
||||
}
|
||||
|
||||
func ExampleSubscriptionIterator_Next() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// List all subscriptions of the project.
|
||||
it := client.Subscriptions(ctx)
|
||||
for {
|
||||
sub, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(sub)
|
||||
}
|
||||
}
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/pubsub"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
func ExampleNewClient() {
|
||||
ctx := context.Background()
|
||||
_, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
// See the other examples to learn how to use the Client.
|
||||
}
|
||||
|
||||
func ExampleClient_CreateTopic() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
// Create a new topic with the given name.
|
||||
topic, err := client.CreateTopic(ctx, "topicName")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
_ = topic // TODO: use the topic.
|
||||
}
|
||||
|
||||
// Use TopicInProject to refer to a topic that is not in the client's project, such
|
||||
// as a public topic.
|
||||
func ExampleClient_TopicInProject() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
topic := client.TopicInProject("topicName", "another-project-id")
|
||||
_ = topic // TODO: use the topic.
|
||||
}
|
||||
|
||||
func ExampleClient_CreateSubscription() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
// Create a new topic with the given name.
|
||||
topic, err := client.CreateTopic(ctx, "topicName")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
// Create a new subscription to the previously created topic
|
||||
// with the given name.
|
||||
sub, err := client.CreateSubscription(ctx, "subName", pubsub.SubscriptionConfig{
|
||||
Topic: topic,
|
||||
AckDeadline: 10 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
_ = sub // TODO: use the subscription.
|
||||
}
|
||||
|
||||
func ExampleTopic_Delete() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
topic := client.Topic("topicName")
|
||||
if err := topic.Delete(ctx); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleTopic_Exists() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
topic := client.Topic("topicName")
|
||||
ok, err := topic.Exists(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
if !ok {
|
||||
// Topic doesn't exist.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleTopic_Publish() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
topic := client.Topic("topicName")
|
||||
defer topic.Stop()
|
||||
var results []*pubsub.PublishResult
|
||||
r := topic.Publish(ctx, &pubsub.Message{
|
||||
Data: []byte("hello world"),
|
||||
})
|
||||
results = append(results, r)
|
||||
// Do other work ...
|
||||
for _, r := range results {
|
||||
id, err := r.Get(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Printf("Published a message with a message ID: %s\n", id)
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleTopic_Subscriptions() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
topic := client.Topic("topic-name")
|
||||
// List all subscriptions of the topic (maybe of multiple projects).
|
||||
for subs := topic.Subscriptions(ctx); ; {
|
||||
sub, err := subs.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = sub // TODO: use the subscription.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscription_Delete() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
sub := client.Subscription("subName")
|
||||
if err := sub.Delete(ctx); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscription_Exists() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
sub := client.Subscription("subName")
|
||||
ok, err := sub.Exists(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
if !ok {
|
||||
// Subscription doesn't exist.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscription_Config() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
config, err := sub.Config(ctx)
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(config)
|
||||
}
|
||||
|
||||
func ExampleSubscription_Receive() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
err = sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) {
|
||||
// TODO: Handle message.
|
||||
// NOTE: May be called concurrently; synchronize access to shared memory.
|
||||
m.Ack()
|
||||
})
|
||||
if err != context.Canceled {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
// This example shows how to configure keepalive so that unacknoweldged messages
|
||||
// expire quickly, allowing other subscribers to take them.
|
||||
func ExampleSubscription_Receive_maxExtension() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
// This program is expected to process and acknowledge messages in 30 seconds. If
|
||||
// not, the Pub/Sub API will assume the message is not acknowledged.
|
||||
sub.ReceiveSettings.MaxExtension = 30 * time.Second
|
||||
err = sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) {
|
||||
// TODO: Handle message.
|
||||
m.Ack()
|
||||
})
|
||||
if err != context.Canceled {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
// This example shows how to throttle Subscription.Receive, which aims for high
|
||||
// throughput by default. By limiting the number of messages and/or bytes being
|
||||
// processed at once, you can bound your program's resource consumption.
|
||||
func ExampleSubscription_Receive_maxOutstanding() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
sub.ReceiveSettings.MaxOutstandingMessages = 5
|
||||
sub.ReceiveSettings.MaxOutstandingBytes = 10e6
|
||||
err = sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) {
|
||||
// TODO: Handle message.
|
||||
m.Ack()
|
||||
})
|
||||
if err != context.Canceled {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscription_Update() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
subConfig, err := sub.Update(ctx, pubsub.SubscriptionConfigToUpdate{
|
||||
PushConfig: &pubsub.PushConfig{Endpoint: "https://example.com/push"},
|
||||
})
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = subConfig // TODO: Use SubscriptionConfig.
|
||||
}
|
||||
|
||||
func ExampleSubscription_CreateSnapshot() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
snapConfig, err := sub.CreateSnapshot(ctx, "snapshotName")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = snapConfig // TODO: Use SnapshotConfig.
|
||||
}
|
||||
|
||||
func ExampleSubscription_SeekToSnapshot() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
snap := client.Snapshot("snapshotName")
|
||||
if err := sub.SeekToSnapshot(ctx, snap); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSubscription_SeekToTime() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
sub := client.Subscription("subName")
|
||||
if err := sub.SeekToTime(ctx, time.Now().Add(-time.Hour)); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleSnapshot_Delete() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
|
||||
snap := client.Snapshot("snapshotName")
|
||||
if err := snap.Delete(ctx); err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleClient_Snapshots() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// List all snapshots for the project.
|
||||
iter := client.Snapshots(ctx)
|
||||
_ = iter // TODO: iterate using Next.
|
||||
}
|
||||
|
||||
func ExampleSnapshotConfigIterator_Next() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// List all snapshots for the project.
|
||||
iter := client.Snapshots(ctx)
|
||||
for {
|
||||
snapConfig, err := iter.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
_ = snapConfig // TODO: use the SnapshotConfig.
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jba): write an example for PublishResult.Ready
|
||||
// TODO(jba): write an example for Subscription.IAM
|
||||
// TODO(jba): write an example for Topic.IAM
|
||||
// TODO(jba): write an example for Topic.Stop
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"cloud.google.com/go/pubsub"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
func ExampleClient_Topics() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
it := client.Topics(ctx)
|
||||
_ = it // TODO: iterate using Next.
|
||||
}
|
||||
|
||||
func ExampleTopicIterator_Next() {
|
||||
ctx := context.Background()
|
||||
client, err := pubsub.NewClient(ctx, "project-id")
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// List all topics.
|
||||
it := client.Topics(ctx)
|
||||
for {
|
||||
t, err := it.Next()
|
||||
if err == iterator.Done {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
fmt.Println(t)
|
||||
}
|
||||
}
|
||||
+322
@@ -0,0 +1,322 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
// This file provides a fake/mock in-memory pubsub server.
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
durpb "github.com/golang/protobuf/ptypes/duration"
|
||||
emptypb "github.com/golang/protobuf/ptypes/empty"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type fakeServer struct {
|
||||
pb.PublisherServer
|
||||
pb.SubscriberServer
|
||||
|
||||
Addr string
|
||||
|
||||
mu sync.Mutex
|
||||
Acked map[string]bool // acked message IDs
|
||||
Deadlines map[string]int32 // deadlines by message ID
|
||||
pullResponses []*pullResponse
|
||||
wg sync.WaitGroup
|
||||
subs map[string]*pb.Subscription
|
||||
topics map[string]*pb.Topic
|
||||
}
|
||||
|
||||
type pullResponse struct {
|
||||
msgs []*pb.ReceivedMessage
|
||||
err error
|
||||
}
|
||||
|
||||
func newFakeServer() (*fakeServer, error) {
|
||||
srv, err := testutil.NewServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fake := &fakeServer{
|
||||
Addr: srv.Addr,
|
||||
Acked: map[string]bool{},
|
||||
Deadlines: map[string]int32{},
|
||||
subs: map[string]*pb.Subscription{},
|
||||
topics: map[string]*pb.Topic{},
|
||||
}
|
||||
pb.RegisterPublisherServer(srv.Gsrv, fake)
|
||||
pb.RegisterSubscriberServer(srv.Gsrv, fake)
|
||||
srv.Start()
|
||||
return fake, nil
|
||||
}
|
||||
|
||||
// Each call to addStreamingPullMessages results in one StreamingPullResponse.
|
||||
func (s *fakeServer) addStreamingPullMessages(msgs []*pb.ReceivedMessage) {
|
||||
s.pullResponses = append(s.pullResponses, &pullResponse{msgs, nil})
|
||||
}
|
||||
|
||||
func (s *fakeServer) addStreamingPullError(err error) {
|
||||
s.pullResponses = append(s.pullResponses, &pullResponse{nil, err})
|
||||
}
|
||||
|
||||
func (s *fakeServer) wait() {
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *fakeServer) StreamingPull(stream pb.Subscriber_StreamingPullServer) error {
|
||||
s.wg.Add(1)
|
||||
defer s.wg.Done()
|
||||
errc := make(chan error, 1)
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
req, err := stream.Recv()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
s.mu.Lock()
|
||||
for _, id := range req.AckIds {
|
||||
s.Acked[id] = true
|
||||
}
|
||||
for i, id := range req.ModifyDeadlineAckIds {
|
||||
s.Deadlines[id] = req.ModifyDeadlineSeconds[i]
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
// Send responses.
|
||||
for {
|
||||
s.mu.Lock()
|
||||
if len(s.pullResponses) == 0 {
|
||||
s.mu.Unlock()
|
||||
// Nothing to send, so wait for the client to shut down the stream.
|
||||
err := <-errc // a real error, or at least EOF
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
pr := s.pullResponses[0]
|
||||
s.pullResponses = s.pullResponses[1:]
|
||||
s.mu.Unlock()
|
||||
if pr.err != nil {
|
||||
// Add a slight delay to ensure the server receives any
|
||||
// messages en route from the client before shutting down the stream.
|
||||
// This reduces flakiness of tests involving retry.
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
if pr.err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if pr.err != nil {
|
||||
return pr.err
|
||||
}
|
||||
// Return any error from Recv.
|
||||
select {
|
||||
case err := <-errc:
|
||||
return err
|
||||
default:
|
||||
}
|
||||
res := &pb.StreamingPullResponse{ReceivedMessages: pr.msgs}
|
||||
if err := stream.Send(res); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
minMessageRetentionDuration = 10 * time.Minute
|
||||
maxMessageRetentionDuration = 168 * time.Hour
|
||||
)
|
||||
|
||||
var defaultMessageRetentionDuration = ptypes.DurationProto(maxMessageRetentionDuration)
|
||||
|
||||
func checkMRD(pmrd *durpb.Duration) error {
|
||||
mrd, err := ptypes.Duration(pmrd)
|
||||
if err != nil || mrd < minMessageRetentionDuration || mrd > maxMessageRetentionDuration {
|
||||
return status.Errorf(codes.InvalidArgument, "bad message_retention_duration %+v", pmrd)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkAckDeadline(ads int32) error {
|
||||
if ads < 10 || ads > 600 {
|
||||
// PubSub service returns Unknown.
|
||||
return status.Errorf(codes.Unknown, "bad ack_deadline_seconds: %d", ads)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) CreateSubscription(ctx context.Context, sub *pb.Subscription) (*pb.Subscription, error) {
|
||||
if s.subs[sub.Name] != nil {
|
||||
return nil, status.Errorf(codes.AlreadyExists, "subscription %q", sub.Name)
|
||||
}
|
||||
sub2 := proto.Clone(sub).(*pb.Subscription)
|
||||
if err := checkAckDeadline(sub.AckDeadlineSeconds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sub.MessageRetentionDuration == nil {
|
||||
sub2.MessageRetentionDuration = defaultMessageRetentionDuration
|
||||
}
|
||||
if err := checkMRD(sub2.MessageRetentionDuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sub.PushConfig == nil {
|
||||
sub2.PushConfig = &pb.PushConfig{}
|
||||
}
|
||||
s.subs[sub.Name] = sub2
|
||||
return sub2, nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) GetSubscription(ctx context.Context, req *pb.GetSubscriptionRequest) (*pb.Subscription, error) {
|
||||
if sub := s.subs[req.Subscription]; sub != nil {
|
||||
return sub, nil
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "subscription %q", req.Subscription)
|
||||
}
|
||||
|
||||
func (s *fakeServer) UpdateSubscription(ctx context.Context, req *pb.UpdateSubscriptionRequest) (*pb.Subscription, error) {
|
||||
sub := s.subs[req.Subscription.Name]
|
||||
if sub == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "subscription %q", req.Subscription.Name)
|
||||
}
|
||||
for _, path := range req.UpdateMask.Paths {
|
||||
switch path {
|
||||
case "push_config":
|
||||
sub.PushConfig = req.Subscription.PushConfig
|
||||
|
||||
case "ack_deadline_seconds":
|
||||
a := req.Subscription.AckDeadlineSeconds
|
||||
if err := checkAckDeadline(a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.AckDeadlineSeconds = a
|
||||
|
||||
case "retain_acked_messages":
|
||||
sub.RetainAckedMessages = req.Subscription.RetainAckedMessages
|
||||
|
||||
case "message_retention_duration":
|
||||
if err := checkMRD(req.Subscription.MessageRetentionDuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.MessageRetentionDuration = req.Subscription.MessageRetentionDuration
|
||||
|
||||
// TODO(jba): labels
|
||||
default:
|
||||
return nil, status.Errorf(codes.InvalidArgument, "unknown field name %q", path)
|
||||
}
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) DeleteSubscription(_ context.Context, req *pb.DeleteSubscriptionRequest) (*emptypb.Empty, error) {
|
||||
if s.subs[req.Subscription] == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "subscription %q", req.Subscription)
|
||||
}
|
||||
delete(s.subs, req.Subscription)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) CreateTopic(_ context.Context, t *pb.Topic) (*pb.Topic, error) {
|
||||
if s.topics[t.Name] != nil {
|
||||
return nil, status.Errorf(codes.AlreadyExists, "topic %q", t.Name)
|
||||
}
|
||||
t2 := proto.Clone(t).(*pb.Topic)
|
||||
s.topics[t.Name] = t2
|
||||
return t2, nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) GetTopic(_ context.Context, req *pb.GetTopicRequest) (*pb.Topic, error) {
|
||||
if t := s.topics[req.Topic]; t != nil {
|
||||
return t, nil
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "topic %q", req.Topic)
|
||||
}
|
||||
|
||||
func (s *fakeServer) DeleteTopic(_ context.Context, req *pb.DeleteTopicRequest) (*emptypb.Empty, error) {
|
||||
if s.topics[req.Topic] == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "topic %q", req.Topic)
|
||||
}
|
||||
delete(s.topics, req.Topic)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) ListTopics(_ context.Context, req *pb.ListTopicsRequest) (*pb.ListTopicsResponse, error) {
|
||||
var names []string
|
||||
for n := range s.topics {
|
||||
if strings.HasPrefix(n, req.Project) {
|
||||
names = append(names, n)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
from, to, nextToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(names))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &pb.ListTopicsResponse{NextPageToken: nextToken}
|
||||
for i := from; i < to; i++ {
|
||||
res.Topics = append(res.Topics, s.topics[names[i]])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) ListSubscriptions(_ context.Context, req *pb.ListSubscriptionsRequest) (*pb.ListSubscriptionsResponse, error) {
|
||||
var names []string
|
||||
for _, sub := range s.subs {
|
||||
if strings.HasPrefix(sub.Name, req.Project) {
|
||||
names = append(names, sub.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
from, to, nextToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(names))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &pb.ListSubscriptionsResponse{NextPageToken: nextToken}
|
||||
for i := from; i < to; i++ {
|
||||
res.Subscriptions = append(res.Subscriptions, s.subs[names[i]])
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *fakeServer) ListTopicSubscriptions(_ context.Context, req *pb.ListTopicSubscriptionsRequest) (*pb.ListTopicSubscriptionsResponse, error) {
|
||||
var names []string
|
||||
for _, sub := range s.subs {
|
||||
if sub.Topic == req.Topic {
|
||||
names = append(names, sub.Name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
from, to, nextToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(names))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.ListTopicSubscriptionsResponse{
|
||||
Subscriptions: names[from:to],
|
||||
NextPageToken: nextToken,
|
||||
}, nil
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/semaphore"
|
||||
)
|
||||
|
||||
// flowController implements flow control for Subscription.Receive.
|
||||
type flowController struct {
|
||||
maxSize int // max total size of messages
|
||||
semCount, semSize *semaphore.Weighted // enforces max number and size of messages
|
||||
}
|
||||
|
||||
// newFlowController creates a new flowController that ensures no more than
|
||||
// maxCount messages or maxSize bytes are outstanding at once. If maxCount or
|
||||
// maxSize is < 1, then an unlimited number of messages or bytes is permitted,
|
||||
// respectively.
|
||||
func newFlowController(maxCount, maxSize int) *flowController {
|
||||
fc := &flowController{
|
||||
maxSize: maxSize,
|
||||
semCount: nil,
|
||||
semSize: nil,
|
||||
}
|
||||
if maxCount > 0 {
|
||||
fc.semCount = semaphore.NewWeighted(int64(maxCount))
|
||||
}
|
||||
if maxSize > 0 {
|
||||
fc.semSize = semaphore.NewWeighted(int64(maxSize))
|
||||
}
|
||||
return fc
|
||||
}
|
||||
|
||||
// acquire blocks until one message of size bytes can proceed or ctx is done.
|
||||
// It returns nil in the first case, or ctx.Err() in the second.
|
||||
//
|
||||
// acquire allows large messages to proceed by treating a size greater than maxSize
|
||||
// as if it were equal to maxSize.
|
||||
func (f *flowController) acquire(ctx context.Context, size int) error {
|
||||
if f.semCount != nil {
|
||||
if err := f.semCount.Acquire(ctx, 1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if f.semSize != nil {
|
||||
if err := f.semSize.Acquire(ctx, f.bound(size)); err != nil {
|
||||
if f.semCount != nil {
|
||||
f.semCount.Release(1)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryAcquire returns false if acquire would block. Otherwise, it behaves like
|
||||
// acquire and returns true.
|
||||
//
|
||||
// tryAcquire allows large messages to proceed by treating a size greater than
|
||||
// maxSize as if it were equal to maxSize.
|
||||
func (f *flowController) tryAcquire(size int) bool {
|
||||
if f.semCount != nil {
|
||||
if !f.semCount.TryAcquire(1) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if f.semSize != nil {
|
||||
if !f.semSize.TryAcquire(f.bound(size)) {
|
||||
if f.semCount != nil {
|
||||
f.semCount.Release(1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// release notes that one message of size bytes is no longer outstanding.
|
||||
func (f *flowController) release(size int) {
|
||||
if f.semCount != nil {
|
||||
f.semCount.Release(1)
|
||||
}
|
||||
if f.semSize != nil {
|
||||
f.semSize.Release(f.bound(size))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *flowController) bound(size int) int64 {
|
||||
if size > f.maxSize {
|
||||
return int64(f.maxSize)
|
||||
}
|
||||
return int64(size)
|
||||
}
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func TestFlowControllerCancel(t *testing.T) {
|
||||
// Test canceling a flow controller's context.
|
||||
t.Parallel()
|
||||
fc := newFlowController(3, 10)
|
||||
if err := fc.acquire(context.Background(), 5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Experiment: a context that times out should always return an error.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Millisecond)
|
||||
defer cancel()
|
||||
if err := fc.acquire(ctx, 6); err != context.DeadlineExceeded {
|
||||
t.Fatalf("got %v, expected DeadlineExceeded", err)
|
||||
}
|
||||
// Control: a context that is not done should always return nil.
|
||||
go func() {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
fc.release(5)
|
||||
}()
|
||||
if err := fc.acquire(context.Background(), 6); err != nil {
|
||||
t.Errorf("got %v, expected nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowControllerLargeRequest(t *testing.T) {
|
||||
// Large requests succeed, consuming the entire allotment.
|
||||
t.Parallel()
|
||||
fc := newFlowController(3, 10)
|
||||
err := fc.acquire(context.Background(), 11)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowControllerNoStarve(t *testing.T) {
|
||||
// A large request won't starve, because the flowController is
|
||||
// (best-effort) FIFO.
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
fc := newFlowController(10, 10)
|
||||
first := make(chan int)
|
||||
for i := 0; i < 20; i++ {
|
||||
go func() {
|
||||
for {
|
||||
if err := fc.acquire(ctx, 1); err != nil {
|
||||
if err != context.Canceled {
|
||||
t.Error(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
select {
|
||||
case first <- 1:
|
||||
default:
|
||||
}
|
||||
fc.release(1)
|
||||
}
|
||||
}()
|
||||
}
|
||||
<-first // Wait until the flowController's state is non-zero.
|
||||
if err := fc.acquire(ctx, 11); err != nil {
|
||||
t.Errorf("got %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowControllerSaturation(t *testing.T) {
|
||||
t.Parallel()
|
||||
const (
|
||||
maxCount = 6
|
||||
maxSize = 10
|
||||
)
|
||||
for _, test := range []struct {
|
||||
acquireSize int
|
||||
wantCount, wantSize int64
|
||||
}{
|
||||
{
|
||||
// Many small acquires cause the flow controller to reach its max count.
|
||||
acquireSize: 1,
|
||||
wantCount: 6,
|
||||
wantSize: 6,
|
||||
},
|
||||
{
|
||||
// Five acquires of size 2 will cause the flow controller to reach its max size,
|
||||
// but not its max count.
|
||||
acquireSize: 2,
|
||||
wantCount: 5,
|
||||
wantSize: 10,
|
||||
},
|
||||
{
|
||||
// If the requests are the right size (relatively prime to maxSize),
|
||||
// the flow controller will not saturate on size. (In this case, not on count either.)
|
||||
acquireSize: 3,
|
||||
wantCount: 3,
|
||||
wantSize: 9,
|
||||
},
|
||||
} {
|
||||
fc := newFlowController(maxCount, maxSize)
|
||||
// Atomically track flow controller state.
|
||||
var curCount, curSize int64
|
||||
success := errors.New("")
|
||||
// Time out if wantSize or wantCount is never reached.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
g, ctx := errgroup.WithContext(ctx)
|
||||
for i := 0; i < 10; i++ {
|
||||
g.Go(func() error {
|
||||
var hitCount, hitSize bool
|
||||
// Run at least until we hit the expected values, and at least
|
||||
// for enough iterations to exceed them if the flow controller
|
||||
// is broken.
|
||||
for i := 0; i < 100 || !hitCount || !hitSize; i++ {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
default:
|
||||
}
|
||||
if err := fc.acquire(ctx, test.acquireSize); err != nil {
|
||||
return err
|
||||
}
|
||||
c := atomic.AddInt64(&curCount, 1)
|
||||
if c > test.wantCount {
|
||||
return fmt.Errorf("count %d exceeds want %d", c, test.wantCount)
|
||||
}
|
||||
if c == test.wantCount {
|
||||
hitCount = true
|
||||
}
|
||||
s := atomic.AddInt64(&curSize, int64(test.acquireSize))
|
||||
if s > test.wantSize {
|
||||
return fmt.Errorf("size %d exceeds want %d", s, test.wantSize)
|
||||
}
|
||||
if s == test.wantSize {
|
||||
hitSize = true
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond) // Let other goroutines make progress.
|
||||
if atomic.AddInt64(&curCount, -1) < 0 {
|
||||
return errors.New("negative count")
|
||||
}
|
||||
if atomic.AddInt64(&curSize, -int64(test.acquireSize)) < 0 {
|
||||
return errors.New("negative size")
|
||||
}
|
||||
fc.release(test.acquireSize)
|
||||
}
|
||||
return success
|
||||
})
|
||||
}
|
||||
if err := g.Wait(); err != success {
|
||||
t.Errorf("%+v: %v", test, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowControllerTryAcquire(t *testing.T) {
|
||||
fc := newFlowController(3, 10)
|
||||
|
||||
// Successfully tryAcquire 4 bytes.
|
||||
if !fc.tryAcquire(4) {
|
||||
t.Error("got false, wanted true")
|
||||
}
|
||||
|
||||
// Fail to tryAcquire 7 bytes.
|
||||
if fc.tryAcquire(7) {
|
||||
t.Error("got true, wanted false")
|
||||
}
|
||||
|
||||
// Successfully tryAcquire 6 byte.
|
||||
if !fc.tryAcquire(6) {
|
||||
t.Error("got false, wanted true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowControllerUnboundedCount(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fc := newFlowController(0, 10)
|
||||
|
||||
// Successfully acquire 4 bytes.
|
||||
if err := fc.acquire(ctx, 4); err != nil {
|
||||
t.Errorf("got %v, wanted no error", err)
|
||||
}
|
||||
|
||||
// Successfully tryAcquire 4 bytes.
|
||||
if !fc.tryAcquire(4) {
|
||||
t.Error("got false, wanted true")
|
||||
}
|
||||
|
||||
// Fail to tryAcquire 3 bytes.
|
||||
if fc.tryAcquire(3) {
|
||||
t.Error("got true, wanted false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowControllerUnboundedBytes(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
fc := newFlowController(2, 0)
|
||||
|
||||
// Successfully acquire 4GB.
|
||||
if err := fc.acquire(ctx, 4e9); err != nil {
|
||||
t.Errorf("got %v, wanted no error", err)
|
||||
}
|
||||
|
||||
// Successfully tryAcquire 4GB bytes.
|
||||
if !fc.tryAcquire(4e9) {
|
||||
t.Error("got false, wanted true")
|
||||
}
|
||||
|
||||
// Fail to tryAcquire a third message.
|
||||
if fc.tryAcquire(3) {
|
||||
t.Error("got true, wanted false")
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"go.opencensus.io/plugin/ocgrpc"
|
||||
"go.opencensus.io/stats"
|
||||
"go.opencensus.io/stats/view"
|
||||
"go.opencensus.io/tag"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func openCensusOptions() []option.ClientOption {
|
||||
return []option.ClientOption{
|
||||
option.WithGRPCDialOption(grpc.WithStatsHandler(&ocgrpc.ClientHandler{})),
|
||||
}
|
||||
}
|
||||
|
||||
var subscriptionKey tag.Key
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
if subscriptionKey, err = tag.NewKey("subscription"); err != nil {
|
||||
log.Fatal("cannot create 'subscription' key")
|
||||
}
|
||||
}
|
||||
|
||||
const statsPrefix = "cloud.google.com/go/pubsub/"
|
||||
|
||||
var (
|
||||
// PullCount is a measure of the number of messages pulled.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
PullCount = stats.Int64(statsPrefix+"pull_count", "Number of PubSub messages pulled", stats.UnitNone)
|
||||
|
||||
// AckCount is a measure of the number of messages acked.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
AckCount = stats.Int64(statsPrefix+"ack_count", "Number of PubSub messages acked", stats.UnitNone)
|
||||
|
||||
// NackCount is a measure of the number of messages nacked.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
NackCount = stats.Int64(statsPrefix+"nack_count", "Number of PubSub messages nacked", stats.UnitNone)
|
||||
|
||||
// ModAckCount is a measure of the number of messages whose ack-deadline was modified.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
ModAckCount = stats.Int64(statsPrefix+"mod_ack_count", "Number of ack-deadlines modified", stats.UnitNone)
|
||||
|
||||
// StreamOpenCount is a measure of the number of times a streaming-pull stream was opened.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamOpenCount = stats.Int64(statsPrefix+"stream_open_count", "Number of calls opening a new streaming pull", stats.UnitNone)
|
||||
|
||||
// StreamRetryCount is a measure of the number of times a streaming-pull operation was retried.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamRetryCount = stats.Int64(statsPrefix+"stream_retry_count", "Number of retries of a stream send or receive", stats.UnitNone)
|
||||
|
||||
// StreamRequestCount is a measure of the number of requests sent on a streaming-pull stream.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamRequestCount = stats.Int64(statsPrefix+"stream_request_count", "Number gRPC StreamingPull request messages sent", stats.UnitNone)
|
||||
|
||||
// StreamResponseCount is a measure of the number of responses received on a streaming-pull stream.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamResponseCount = stats.Int64(statsPrefix+"stream_response_count", "Number of gRPC StreamingPull response messages received", stats.UnitNone)
|
||||
|
||||
// PullCountView is a cumulative sum of PullCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
PullCountView *view.View
|
||||
|
||||
// AckCountView is a cumulative sum of AckCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
AckCountView *view.View
|
||||
|
||||
// NackCountView is a cumulative sum of NackCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
NackCountView *view.View
|
||||
|
||||
// ModAckCountView is a cumulative sum of ModAckCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
ModAckCountView *view.View
|
||||
|
||||
// StreamOpenCountView is a cumulative sum of StreamOpenCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamOpenCountView *view.View
|
||||
|
||||
// StreamRetryCountView is a cumulative sum of StreamRetryCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamRetryCountView *view.View
|
||||
|
||||
// StreamRequestCountView is a cumulative sum of StreamRequestCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamRequestCountView *view.View
|
||||
|
||||
// StreamResponseCountView is a cumulative sum of StreamResponseCount.
|
||||
// It is EXPERIMENTAL and subject to change or removal without notice.
|
||||
StreamResponseCountView *view.View
|
||||
)
|
||||
|
||||
func init() {
|
||||
PullCountView = countView(PullCount)
|
||||
AckCountView = countView(AckCount)
|
||||
NackCountView = countView(NackCount)
|
||||
ModAckCountView = countView(ModAckCount)
|
||||
StreamOpenCountView = countView(StreamOpenCount)
|
||||
StreamRetryCountView = countView(StreamRetryCount)
|
||||
StreamRequestCountView = countView(StreamRequestCount)
|
||||
StreamResponseCountView = countView(StreamResponseCount)
|
||||
}
|
||||
|
||||
func countView(m *stats.Int64Measure) *view.View {
|
||||
return &view.View{
|
||||
Name: m.Name(),
|
||||
Description: m.Description(),
|
||||
TagKeys: []tag.Key{subscriptionKey},
|
||||
Measure: m,
|
||||
Aggregation: view.Sum(),
|
||||
}
|
||||
}
|
||||
|
||||
var logOnce sync.Once
|
||||
|
||||
func withSubscriptionKey(ctx context.Context, subName string) context.Context {
|
||||
ctx, err := tag.New(ctx, tag.Upsert(subscriptionKey, subName))
|
||||
if err != nil {
|
||||
logOnce.Do(func() {
|
||||
log.Printf("pubsub: error creating tag map: %v", err)
|
||||
})
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func recordStat(ctx context.Context, m *stats.Int64Measure, n int64) {
|
||||
stats.Record(ctx, m.M(n))
|
||||
}
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
gax "github.com/googleapis/gax-go"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"cloud.google.com/go/iam"
|
||||
"cloud.google.com/go/internal"
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
topicIDs = testutil.NewUIDSpace("topic")
|
||||
subIDs = testutil.NewUIDSpace("sub")
|
||||
)
|
||||
|
||||
// messageData is used to hold the contents of a message so that it can be compared against the contents
|
||||
// of another message without regard to irrelevant fields.
|
||||
type messageData struct {
|
||||
ID string
|
||||
Data []byte
|
||||
Attributes map[string]string
|
||||
}
|
||||
|
||||
func extractMessageData(m *Message) *messageData {
|
||||
return &messageData{
|
||||
ID: m.ID,
|
||||
Data: m.Data,
|
||||
Attributes: m.Attributes,
|
||||
}
|
||||
}
|
||||
|
||||
func integrationTestClient(t *testing.T, ctx context.Context) *Client {
|
||||
if testing.Short() {
|
||||
t.Skip("Integration tests skipped in short mode")
|
||||
}
|
||||
projID := testutil.ProjID()
|
||||
if projID == "" {
|
||||
t.Skip("Integration tests skipped. See CONTRIBUTING.md for details")
|
||||
}
|
||||
ts := testutil.TokenSource(ctx, ScopePubSub, ScopeCloudPlatform)
|
||||
if ts == nil {
|
||||
t.Skip("Integration tests skipped. See CONTRIBUTING.md for details")
|
||||
}
|
||||
client, err := NewClient(ctx, projID, option.WithTokenSource(ts))
|
||||
if err != nil {
|
||||
t.Fatalf("Creating client error: %v", err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := integrationTestClient(t, ctx)
|
||||
defer client.Close()
|
||||
|
||||
topic, err := client.CreateTopic(ctx, topicIDs.New())
|
||||
if err != nil {
|
||||
t.Errorf("CreateTopic error: %v", err)
|
||||
}
|
||||
defer topic.Stop()
|
||||
|
||||
var sub *Subscription
|
||||
if sub, err = client.CreateSubscription(ctx, subIDs.New(), SubscriptionConfig{Topic: topic}); err != nil {
|
||||
t.Errorf("CreateSub error: %v", err)
|
||||
}
|
||||
|
||||
exists, err := topic.Exists(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("TopicExists error: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("topic %v should exist, but it doesn't", topic)
|
||||
}
|
||||
|
||||
exists, err = sub.Exists(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("SubExists error: %v", err)
|
||||
}
|
||||
if !exists {
|
||||
t.Errorf("subscription %s should exist, but it doesn't", sub.ID())
|
||||
}
|
||||
|
||||
var msgs []*Message
|
||||
for i := 0; i < 10; i++ {
|
||||
text := fmt.Sprintf("a message with an index %d", i)
|
||||
attrs := make(map[string]string)
|
||||
attrs["foo"] = "bar"
|
||||
msgs = append(msgs, &Message{
|
||||
Data: []byte(text),
|
||||
Attributes: attrs,
|
||||
})
|
||||
}
|
||||
|
||||
// Publish the messages.
|
||||
type pubResult struct {
|
||||
m *Message
|
||||
r *PublishResult
|
||||
}
|
||||
var rs []pubResult
|
||||
for _, m := range msgs {
|
||||
r := topic.Publish(ctx, m)
|
||||
rs = append(rs, pubResult{m, r})
|
||||
}
|
||||
want := make(map[string]*messageData)
|
||||
for _, res := range rs {
|
||||
id, err := res.r.Get(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
md := extractMessageData(res.m)
|
||||
md.ID = id
|
||||
want[md.ID] = md
|
||||
}
|
||||
|
||||
// Use a timeout to ensure that Pull does not block indefinitely if there are unexpectedly few messages available.
|
||||
timeoutCtx, _ := context.WithTimeout(ctx, time.Minute)
|
||||
gotMsgs, err := pullN(timeoutCtx, sub, len(want), func(ctx context.Context, m *Message) {
|
||||
m.Ack()
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Pull: %v", err)
|
||||
}
|
||||
got := make(map[string]*messageData)
|
||||
for _, m := range gotMsgs {
|
||||
md := extractMessageData(m)
|
||||
got[md.ID] = md
|
||||
}
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Errorf("messages: got: %v ; want: %v", got, want)
|
||||
}
|
||||
|
||||
if msg, ok := testIAM(ctx, topic.IAM(), "pubsub.topics.get"); !ok {
|
||||
t.Errorf("topic IAM: %s", msg)
|
||||
}
|
||||
if msg, ok := testIAM(ctx, sub.IAM(), "pubsub.subscriptions.get"); !ok {
|
||||
t.Errorf("sub IAM: %s", msg)
|
||||
}
|
||||
|
||||
snap, err := sub.CreateSnapshot(ctx, "")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSnapshot error: %v", err)
|
||||
}
|
||||
|
||||
timeoutCtx, _ = context.WithTimeout(ctx, time.Minute)
|
||||
err = internal.Retry(timeoutCtx, gax.Backoff{}, func() (bool, error) {
|
||||
snapIt := client.Snapshots(timeoutCtx)
|
||||
for {
|
||||
s, err := snapIt.Next()
|
||||
if err == nil && s.name == snap.name {
|
||||
return true, nil
|
||||
}
|
||||
if err == iterator.Done {
|
||||
return false, fmt.Errorf("cannot find snapshot: %q", snap.name)
|
||||
}
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = internal.Retry(timeoutCtx, gax.Backoff{}, func() (bool, error) {
|
||||
err := sub.SeekToSnapshot(timeoutCtx, snap.Snapshot)
|
||||
return err == nil, err
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = internal.Retry(timeoutCtx, gax.Backoff{}, func() (bool, error) {
|
||||
err := sub.SeekToTime(timeoutCtx, time.Now())
|
||||
return err == nil, err
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = internal.Retry(timeoutCtx, gax.Backoff{}, func() (bool, error) {
|
||||
snapHandle := client.Snapshot(snap.ID())
|
||||
err := snapHandle.Delete(timeoutCtx)
|
||||
return err == nil, err
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := sub.Delete(ctx); err != nil {
|
||||
t.Errorf("DeleteSub error: %v", err)
|
||||
}
|
||||
|
||||
if err := topic.Delete(ctx); err != nil {
|
||||
t.Errorf("DeleteTopic error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// IAM tests.
|
||||
// NOTE: for these to succeed, the test runner identity must have the Pub/Sub Admin or Owner roles.
|
||||
// To set, visit https://console.developers.google.com, select "IAM & Admin" from the top-left
|
||||
// menu, choose the account, click the Roles dropdown, and select "Pub/Sub > Pub/Sub Admin".
|
||||
// TODO(jba): move this to a testing package within cloud.google.com/iam, so we can re-use it.
|
||||
func testIAM(ctx context.Context, h *iam.Handle, permission string) (msg string, ok bool) {
|
||||
// Attempting to add an non-existent identity (e.g. "alice@example.com") causes the service
|
||||
// to return an internal error, so use a real identity.
|
||||
const member = "domain:google.com"
|
||||
|
||||
var policy *iam.Policy
|
||||
var err error
|
||||
|
||||
if policy, err = h.Policy(ctx); err != nil {
|
||||
return fmt.Sprintf("Policy: %v", err), false
|
||||
}
|
||||
// The resource is new, so the policy should be empty.
|
||||
if got := policy.Roles(); len(got) > 0 {
|
||||
return fmt.Sprintf("initially: got roles %v, want none", got), false
|
||||
}
|
||||
// Add a member, set the policy, then check that the member is present.
|
||||
policy.Add(member, iam.Viewer)
|
||||
if err := h.SetPolicy(ctx, policy); err != nil {
|
||||
return fmt.Sprintf("SetPolicy: %v", err), false
|
||||
}
|
||||
if policy, err = h.Policy(ctx); err != nil {
|
||||
return fmt.Sprintf("Policy: %v", err), false
|
||||
}
|
||||
if got, want := policy.Members(iam.Viewer), []string{member}; !testutil.Equal(got, want) {
|
||||
return fmt.Sprintf("after Add: got %v, want %v", got, want), false
|
||||
}
|
||||
// Now remove that member, set the policy, and check that it's empty again.
|
||||
policy.Remove(member, iam.Viewer)
|
||||
if err := h.SetPolicy(ctx, policy); err != nil {
|
||||
return fmt.Sprintf("SetPolicy: %v", err), false
|
||||
}
|
||||
if policy, err = h.Policy(ctx); err != nil {
|
||||
return fmt.Sprintf("Policy: %v", err), false
|
||||
}
|
||||
if got := policy.Roles(); len(got) > 0 {
|
||||
return fmt.Sprintf("after Remove: got roles %v, want none", got), false
|
||||
}
|
||||
// Call TestPermissions.
|
||||
// Because this user is an admin, it has all the permissions on the
|
||||
// resource type. Note: the service fails if we ask for inapplicable
|
||||
// permissions (e.g. a subscription permission on a topic, or a topic
|
||||
// create permission on a topic rather than its parent).
|
||||
wantPerms := []string{permission}
|
||||
gotPerms, err := h.TestPermissions(ctx, wantPerms)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("TestPermissions: %v", err), false
|
||||
}
|
||||
if !testutil.Equal(gotPerms, wantPerms) {
|
||||
return fmt.Sprintf("TestPermissions: got %v, want %v", gotPerms, wantPerms), false
|
||||
}
|
||||
return "", true
|
||||
}
|
||||
|
||||
func TestSubscriptionUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := integrationTestClient(t, ctx)
|
||||
defer client.Close()
|
||||
|
||||
topic, err := client.CreateTopic(ctx, topicIDs.New())
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTopic error: %v", err)
|
||||
}
|
||||
defer topic.Stop()
|
||||
defer topic.Delete(ctx)
|
||||
|
||||
var sub *Subscription
|
||||
if sub, err = client.CreateSubscription(ctx, subIDs.New(), SubscriptionConfig{Topic: topic}); err != nil {
|
||||
t.Fatalf("CreateSub error: %v", err)
|
||||
}
|
||||
defer sub.Delete(ctx)
|
||||
|
||||
got, err := sub.Config(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := SubscriptionConfig{
|
||||
Topic: topic,
|
||||
AckDeadline: 10 * time.Second,
|
||||
RetainAckedMessages: false,
|
||||
RetentionDuration: defaultRetentionDuration,
|
||||
}
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
// Add a PushConfig and change other fields.
|
||||
projID := testutil.ProjID()
|
||||
pc := PushConfig{
|
||||
Endpoint: "https://" + projID + ".appspot.com/_ah/push-handlers/push",
|
||||
Attributes: map[string]string{"x-goog-version": "v1"},
|
||||
}
|
||||
got, err = sub.Update(ctx, SubscriptionConfigToUpdate{
|
||||
PushConfig: &pc,
|
||||
AckDeadline: 2 * time.Minute,
|
||||
RetainAckedMessages: true,
|
||||
RetentionDuration: 2 * time.Hour,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want = SubscriptionConfig{
|
||||
Topic: topic,
|
||||
PushConfig: pc,
|
||||
AckDeadline: 2 * time.Minute,
|
||||
RetainAckedMessages: true,
|
||||
RetentionDuration: 2 * time.Hour,
|
||||
}
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
// Remove the PushConfig, turning the subscription back into pull mode.
|
||||
// Change AckDeadline, but nothing else.
|
||||
pc = PushConfig{}
|
||||
got, err = sub.Update(ctx, SubscriptionConfigToUpdate{
|
||||
PushConfig: &pc,
|
||||
AckDeadline: 30 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want.PushConfig = pc
|
||||
want.AckDeadline = 30 * time.Second
|
||||
// service issue: PushConfig attributes are not removed.
|
||||
// TODO(jba): remove when issue resolved.
|
||||
want.PushConfig.Attributes = map[string]string{"x-goog-version": "v1"}
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
// If nothing changes, our client returns an error.
|
||||
_, err = sub.Update(ctx, SubscriptionConfigToUpdate{})
|
||||
if err == nil {
|
||||
t.Fatal("got nil, wanted error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublicTopic(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := integrationTestClient(t, ctx)
|
||||
defer client.Close()
|
||||
|
||||
sub, err := client.CreateSubscription(ctx, subIDs.New(), SubscriptionConfig{
|
||||
Topic: client.TopicInProject("taxirides-realtime", "pubsub-public-data"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sub.Delete(ctx)
|
||||
// Confirm that Receive works. It doesn't matter if we actually get any
|
||||
// messages.
|
||||
ctxt, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
err = sub.Receive(ctxt, func(_ context.Context, msg *Message) {
|
||||
msg.Ack()
|
||||
cancel()
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegration_Errors(t *testing.T) {
|
||||
// Test various edge conditions.
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
client := integrationTestClient(t, ctx)
|
||||
defer client.Close()
|
||||
|
||||
topic, err := client.CreateTopic(ctx, topicIDs.New())
|
||||
if err != nil {
|
||||
t.Fatalf("CreateTopic error: %v", err)
|
||||
}
|
||||
defer topic.Stop()
|
||||
defer topic.Delete(ctx)
|
||||
|
||||
// Out-of-range retention duration.
|
||||
sub, err := client.CreateSubscription(ctx, subIDs.New(), SubscriptionConfig{
|
||||
Topic: topic,
|
||||
RetentionDuration: 1 * time.Second,
|
||||
})
|
||||
if want := codes.InvalidArgument; grpc.Code(err) != want {
|
||||
t.Errorf("got <%v>, want %s", err, want)
|
||||
}
|
||||
if err == nil {
|
||||
sub.Delete(ctx)
|
||||
}
|
||||
|
||||
// Ack deadline less than minimum.
|
||||
sub, err = client.CreateSubscription(ctx, subIDs.New(), SubscriptionConfig{
|
||||
Topic: topic,
|
||||
AckDeadline: 5 * time.Second,
|
||||
})
|
||||
if want := codes.Unknown; grpc.Code(err) != want {
|
||||
t.Errorf("got <%v>, want %s", err, want)
|
||||
}
|
||||
if err == nil {
|
||||
sub.Delete(ctx)
|
||||
}
|
||||
|
||||
// Updating a non-existent subscription.
|
||||
sub = client.Subscription(subIDs.New())
|
||||
_, err = sub.Update(ctx, SubscriptionConfigToUpdate{AckDeadline: 20 * time.Second})
|
||||
if want := codes.NotFound; grpc.Code(err) != want {
|
||||
t.Errorf("got <%v>, want %s", err, want)
|
||||
}
|
||||
// Deleting a non-existent subscription.
|
||||
err = sub.Delete(ctx)
|
||||
if want := codes.NotFound; grpc.Code(err) != want {
|
||||
t.Errorf("got <%v>, want %s", err, want)
|
||||
}
|
||||
|
||||
// Updating out-of-range retention duration.
|
||||
sub, err = client.CreateSubscription(ctx, subIDs.New(), SubscriptionConfig{Topic: topic})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sub.Delete(ctx)
|
||||
_, err = sub.Update(ctx, SubscriptionConfigToUpdate{RetentionDuration: 1000 * time.Hour})
|
||||
if want := codes.InvalidArgument; grpc.Code(err) != want {
|
||||
t.Errorf("got <%v>, want %s", err, want)
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// D is a distribution. Methods of D can be called concurrently by multiple
|
||||
// goroutines.
|
||||
type D struct {
|
||||
buckets []uint64
|
||||
}
|
||||
|
||||
// New creates a new distribution capable of holding values from 0 to n-1.
|
||||
func New(n int) *D {
|
||||
return &D{
|
||||
buckets: make([]uint64, n),
|
||||
}
|
||||
}
|
||||
|
||||
// Record records value v to the distribution.
|
||||
// To help with distributions with long tails, if v is larger than the maximum value,
|
||||
// Record records the maximum value instead.
|
||||
// If v is negative, Record panics.
|
||||
func (d *D) Record(v int) {
|
||||
if v < 0 {
|
||||
log.Panicf("Record: value out of range: %d", v)
|
||||
} else if v >= len(d.buckets) {
|
||||
v = len(d.buckets) - 1
|
||||
}
|
||||
atomic.AddUint64(&d.buckets[v], 1)
|
||||
}
|
||||
|
||||
// Percentile computes the p-th percentile of the distribution where
|
||||
// p is between 0 and 1.
|
||||
func (d *D) Percentile(p float64) int {
|
||||
// NOTE: This implementation uses the nearest-rank method.
|
||||
// https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method
|
||||
|
||||
if p < 0 || p > 1 {
|
||||
log.Panicf("Percentile: percentile out of range: %f", p)
|
||||
}
|
||||
|
||||
bucketSums := make([]uint64, len(d.buckets))
|
||||
var sum uint64
|
||||
for i := range bucketSums {
|
||||
sum += atomic.LoadUint64(&d.buckets[i])
|
||||
bucketSums[i] = sum
|
||||
}
|
||||
|
||||
total := bucketSums[len(bucketSums)-1]
|
||||
target := uint64(math.Ceil(float64(total) * p))
|
||||
return sort.Search(len(bucketSums), func(i int) bool { return bucketSums[i] >= target })
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package distribution
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDistribution(t *testing.T) {
|
||||
// These tests come from examples in https://en.wikipedia.org/wiki/Percentile#The_nearest-rank_method
|
||||
tests := []struct {
|
||||
// values in distribution
|
||||
vals []int
|
||||
|
||||
// percentiles and expected percentile values
|
||||
pp []float64
|
||||
vv []int
|
||||
}{
|
||||
{
|
||||
vals: []int{15, 20, 35, 40, 50},
|
||||
pp: []float64{0.05, 0.3, 0.4, 0.5, 1},
|
||||
vv: []int{15, 20, 20, 35, 50},
|
||||
},
|
||||
{
|
||||
vals: []int{3, 6, 7, 8, 8, 10, 13, 15, 16, 20},
|
||||
pp: []float64{0.25, 0.5, 0.75, 1},
|
||||
vv: []int{7, 8, 15, 20},
|
||||
},
|
||||
{
|
||||
vals: []int{3, 6, 7, 8, 8, 9, 10, 13, 15, 16, 20},
|
||||
pp: []float64{0.25, 0.5, 0.75, 1},
|
||||
vv: []int{7, 9, 15, 20},
|
||||
},
|
||||
}
|
||||
|
||||
maxVal := 0
|
||||
for _, tst := range tests {
|
||||
for _, v := range tst.vals {
|
||||
if maxVal < v {
|
||||
maxVal = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tst := range tests {
|
||||
d := New(maxVal + 1)
|
||||
for _, v := range tst.vals {
|
||||
d.Record(v)
|
||||
}
|
||||
for i, p := range tst.pp {
|
||||
got, want := d.Percentile(p), tst.vv[i]
|
||||
if got != want {
|
||||
t.Errorf("d=%v, d.Percentile(%f)=%d, want %d", d, p, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRace(t *testing.T) {
|
||||
const N int = 1e3
|
||||
const parallel = 2
|
||||
|
||||
d := New(N)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(parallel)
|
||||
for i := 0; i < parallel; i++ {
|
||||
go func() {
|
||||
for i := 0; i < N; i++ {
|
||||
d.Record(i)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
if p := d.Percentile(0.5); p > N {
|
||||
t.Fatalf("d.Percentile(0.5)=%d, expected to be at most %d", p, N)
|
||||
}
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
vkit "cloud.google.com/go/pubsub/apiv1"
|
||||
"cloud.google.com/go/pubsub/internal/distribution"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
// newMessageIterator starts a new streamingMessageIterator. Stop must be called on the messageIterator
|
||||
// when it is no longer needed.
|
||||
// subName is the full name of the subscription to pull messages from.
|
||||
// ctx is the context to use for acking messages and extending message deadlines.
|
||||
func newMessageIterator(ctx context.Context, subc *vkit.SubscriberClient, subName string, po *pullOptions) *streamingMessageIterator {
|
||||
ps := newPullStream(ctx, subc, subName, int32(po.ackDeadline.Seconds()))
|
||||
return newStreamingMessageIterator(ctx, ps, po)
|
||||
}
|
||||
|
||||
type streamingMessageIterator struct {
|
||||
ctx context.Context
|
||||
po *pullOptions
|
||||
ps *pullStream
|
||||
kaTicker *time.Ticker // keep-alive (deadline extensions)
|
||||
ackTicker *time.Ticker // message acks
|
||||
nackTicker *time.Ticker // message nacks (more frequent than acks)
|
||||
failed chan struct{} // closed on stream error
|
||||
stopped chan struct{} // closed when Stop is called
|
||||
drained chan struct{} // closed when stopped && no more pending messages
|
||||
wg sync.WaitGroup
|
||||
|
||||
mu sync.Mutex
|
||||
ackTimeDist *distribution.D
|
||||
keepAliveDeadlines map[string]time.Time
|
||||
pendingReq *pb.StreamingPullRequest
|
||||
pendingModAcks map[string]int32 // ack IDs whose ack deadline is to be modified
|
||||
err error // error from stream failure
|
||||
}
|
||||
|
||||
func newStreamingMessageIterator(ctx context.Context, ps *pullStream, po *pullOptions) *streamingMessageIterator {
|
||||
// TODO: make kaTicker frequency more configurable. (ackDeadline - 5s) is a
|
||||
// reasonable default for now, because the minimum ack period is 10s. This
|
||||
// gives us 5s grace.
|
||||
keepAlivePeriod := po.ackDeadline - 5*time.Second
|
||||
kaTicker := time.NewTicker(keepAlivePeriod)
|
||||
|
||||
// Ack promptly so users don't lose work if client crashes.
|
||||
ackTicker := time.NewTicker(100 * time.Millisecond)
|
||||
nackTicker := time.NewTicker(100 * time.Millisecond)
|
||||
it := &streamingMessageIterator{
|
||||
ctx: ctx,
|
||||
ps: ps,
|
||||
po: po,
|
||||
kaTicker: kaTicker,
|
||||
ackTicker: ackTicker,
|
||||
nackTicker: nackTicker,
|
||||
failed: make(chan struct{}),
|
||||
stopped: make(chan struct{}),
|
||||
drained: make(chan struct{}),
|
||||
ackTimeDist: distribution.New(int(maxAckDeadline/time.Second) + 1),
|
||||
keepAliveDeadlines: map[string]time.Time{},
|
||||
pendingReq: &pb.StreamingPullRequest{},
|
||||
pendingModAcks: map[string]int32{},
|
||||
}
|
||||
it.wg.Add(1)
|
||||
go it.sender()
|
||||
return it
|
||||
}
|
||||
|
||||
// Subscription.receive will call stop on its messageIterator when finished with it.
|
||||
// Stop will block until Done has been called on all Messages that have been
|
||||
// returned by Next, or until the context with which the messageIterator was created
|
||||
// is cancelled or exceeds its deadline.
|
||||
func (it *streamingMessageIterator) stop() {
|
||||
it.mu.Lock()
|
||||
select {
|
||||
case <-it.stopped:
|
||||
default:
|
||||
close(it.stopped)
|
||||
}
|
||||
it.checkDrained()
|
||||
it.mu.Unlock()
|
||||
it.wg.Wait()
|
||||
}
|
||||
|
||||
// checkDrained closes the drained channel if the iterator has been stopped and all
|
||||
// pending messages have either been n/acked or expired.
|
||||
//
|
||||
// Called with the lock held.
|
||||
func (it *streamingMessageIterator) checkDrained() {
|
||||
select {
|
||||
case <-it.drained:
|
||||
return
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-it.stopped:
|
||||
if len(it.keepAliveDeadlines) == 0 {
|
||||
close(it.drained)
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a message is acked/nacked.
|
||||
func (it *streamingMessageIterator) done(ackID string, ack bool, receiveTime time.Time) {
|
||||
it.ackTimeDist.Record(int(time.Since(receiveTime) / time.Second))
|
||||
it.mu.Lock()
|
||||
defer it.mu.Unlock()
|
||||
delete(it.keepAliveDeadlines, ackID)
|
||||
if ack {
|
||||
it.pendingReq.AckIds = append(it.pendingReq.AckIds, ackID)
|
||||
} else {
|
||||
it.pendingModAcks[ackID] = 0 // Nack indicated by modifying the deadline to zero.
|
||||
}
|
||||
it.checkDrained()
|
||||
}
|
||||
|
||||
// fail is called when a stream method returns a permanent error.
|
||||
func (it *streamingMessageIterator) fail(err error) {
|
||||
it.mu.Lock()
|
||||
if it.err == nil {
|
||||
it.err = err
|
||||
close(it.failed)
|
||||
}
|
||||
it.mu.Unlock()
|
||||
}
|
||||
|
||||
// receive makes a call to the stream's Recv method and returns
|
||||
// its messages.
|
||||
func (it *streamingMessageIterator) receive() ([]*Message, error) {
|
||||
// Stop retrieving messages if the context is done, the stream
|
||||
// failed, or the iterator's Stop method was called.
|
||||
select {
|
||||
case <-it.ctx.Done():
|
||||
return nil, it.ctx.Err()
|
||||
default:
|
||||
}
|
||||
it.mu.Lock()
|
||||
err := it.err
|
||||
it.mu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Receive messages from stream. This may block indefinitely.
|
||||
res, err := it.ps.Recv()
|
||||
// The pullStream handles retries, so any error here is fatal.
|
||||
if err != nil {
|
||||
it.fail(err)
|
||||
return nil, err
|
||||
}
|
||||
msgs, err := convertMessages(res.ReceivedMessages)
|
||||
if err != nil {
|
||||
it.fail(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We received some messages. Remember them so we can keep them alive. Also,
|
||||
// arrange for a receipt mod-ack (which will occur at the next firing of
|
||||
// nackTicker).
|
||||
maxExt := time.Now().Add(it.po.maxExtension)
|
||||
deadline := trunc32(int64(it.po.ackDeadline.Seconds()))
|
||||
it.mu.Lock()
|
||||
now := time.Now()
|
||||
for _, m := range msgs {
|
||||
m.receiveTime = now
|
||||
m.doneFunc = it.done
|
||||
it.keepAliveDeadlines[m.ackID] = maxExt
|
||||
// The receipt mod-ack uses the subscription's configured ack deadline. Don't
|
||||
// change the mod-ack if one is already pending. This is possible if there
|
||||
// are retries.
|
||||
if _, ok := it.pendingModAcks[m.ackID]; !ok {
|
||||
it.pendingModAcks[m.ackID] = deadline
|
||||
}
|
||||
}
|
||||
it.mu.Unlock()
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
// sender runs in a goroutine and handles all sends to the stream.
|
||||
func (it *streamingMessageIterator) sender() {
|
||||
defer it.wg.Done()
|
||||
defer it.kaTicker.Stop()
|
||||
defer it.ackTicker.Stop()
|
||||
defer it.nackTicker.Stop()
|
||||
defer it.ps.CloseSend()
|
||||
|
||||
done := false
|
||||
for !done {
|
||||
send := false
|
||||
select {
|
||||
case <-it.ctx.Done():
|
||||
// Context canceled or timed out: stop immediately, without
|
||||
// another RPC.
|
||||
return
|
||||
|
||||
case <-it.failed:
|
||||
// Stream failed: nothing to do, so stop immediately.
|
||||
return
|
||||
|
||||
case <-it.drained:
|
||||
// All outstanding messages have been marked done:
|
||||
// nothing left to do except send the final request.
|
||||
it.mu.Lock()
|
||||
send = (len(it.pendingReq.AckIds) > 0 || len(it.pendingModAcks) > 0)
|
||||
done = true
|
||||
|
||||
case <-it.kaTicker.C:
|
||||
it.mu.Lock()
|
||||
it.handleKeepAlives()
|
||||
send = (len(it.pendingModAcks) > 0)
|
||||
|
||||
case <-it.nackTicker.C:
|
||||
it.mu.Lock()
|
||||
send = (len(it.pendingModAcks) > 0)
|
||||
|
||||
case <-it.ackTicker.C:
|
||||
it.mu.Lock()
|
||||
send = (len(it.pendingReq.AckIds) > 0)
|
||||
}
|
||||
// Lock is held here.
|
||||
if send {
|
||||
req := it.pendingReq
|
||||
it.pendingReq = &pb.StreamingPullRequest{}
|
||||
modAcks := it.pendingModAcks
|
||||
it.pendingModAcks = map[string]int32{}
|
||||
it.mu.Unlock()
|
||||
for id, s := range modAcks {
|
||||
req.ModifyDeadlineAckIds = append(req.ModifyDeadlineAckIds, id)
|
||||
req.ModifyDeadlineSeconds = append(req.ModifyDeadlineSeconds, s)
|
||||
}
|
||||
err := it.send(req)
|
||||
if err != nil {
|
||||
// The streamingPuller handles retries, so any error here
|
||||
// is fatal to the iterator.
|
||||
it.fail(err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
it.mu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (it *streamingMessageIterator) send(req *pb.StreamingPullRequest) error {
|
||||
// Note: len(modAckIDs) == len(modSecs)
|
||||
var rest *pb.StreamingPullRequest
|
||||
for len(req.AckIds) > 0 || len(req.ModifyDeadlineAckIds) > 0 {
|
||||
req, rest = splitRequest(req, maxPayload)
|
||||
if err := it.ps.Send(req); err != nil {
|
||||
return err
|
||||
}
|
||||
req = rest
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleKeepAlives modifies the pending request to include deadline extensions
|
||||
// for live messages. It also purges expired messages.
|
||||
//
|
||||
// Called with the lock held.
|
||||
func (it *streamingMessageIterator) handleKeepAlives() {
|
||||
now := time.Now()
|
||||
dl := trunc32(int64(it.po.ackDeadline.Seconds()))
|
||||
for id, expiry := range it.keepAliveDeadlines {
|
||||
if expiry.Before(now) {
|
||||
// This delete will not result in skipping any map items, as implied by
|
||||
// the spec at https://golang.org/ref/spec#For_statements, "For
|
||||
// statements with range clause", note 3, and stated explicitly at
|
||||
// https://groups.google.com/forum/#!msg/golang-nuts/UciASUb03Js/pzSq5iVFAQAJ.
|
||||
delete(it.keepAliveDeadlines, id)
|
||||
} else {
|
||||
// This will not overwrite a nack, because nacking removes the ID from keepAliveDeadlines.
|
||||
it.pendingModAcks[id] = dl
|
||||
}
|
||||
}
|
||||
it.checkDrained()
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package loadtest
|
||||
|
||||
// Performance benchmarks for pubsub.
|
||||
// Run with
|
||||
// go test -bench . -cpu 1
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"cloud.google.com/go/pubsub"
|
||||
gtransport "google.golang.org/api/transport/grpc"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
// These constants are designed to match the "throughput" test in
|
||||
// https://github.com/GoogleCloudPlatform/pubsub/blob/master/load-test-framework/run.py
|
||||
// and
|
||||
// https://github.com/GoogleCloudPlatform/pubsub/blob/master/load-test-framework/src/main/java/com/google/pubsub/clients/experimental/CPSPublisherTask.java
|
||||
|
||||
const (
|
||||
nMessages = 1e5
|
||||
messageSize = 10000 // size of msg data in bytes
|
||||
batchSize = 10
|
||||
batchDuration = 50 * time.Millisecond
|
||||
serverDelay = 200 * time.Millisecond
|
||||
maxOutstandingPublishes = 1600 // max_outstanding_messages in run.py
|
||||
)
|
||||
|
||||
func BenchmarkPublishThroughput(b *testing.B) {
|
||||
b.SetBytes(nMessages * messageSize)
|
||||
client := perfClient(serverDelay, 1, b)
|
||||
|
||||
lts := &PubServer{ID: "xxx"}
|
||||
lts.init(client, "t", messageSize, batchSize, batchDuration)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
runOnce(lts)
|
||||
}
|
||||
}
|
||||
|
||||
func runOnce(lts *PubServer) {
|
||||
nRequests := int64(nMessages / batchSize)
|
||||
var nPublished int64
|
||||
var wg sync.WaitGroup
|
||||
// The Java loadtest framework is rate-limited to 1 billion Execute calls a
|
||||
// second (each Execute call corresponding to a publishBatch call here),
|
||||
// but we can ignore this because of the following.
|
||||
// The framework runs 10,000 threads, each calling Execute in a loop, but
|
||||
// we can ignore this too.
|
||||
// The framework caps the number of outstanding calls to Execute at
|
||||
// maxOutstandingPublishes. That is what we simulate here.
|
||||
for i := 0; i < maxOutstandingPublishes; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for atomic.AddInt64(&nRequests, -1) >= 0 {
|
||||
latencies, err := lts.publishBatch()
|
||||
if err != nil {
|
||||
log.Fatalf("publishBatch: %v", err)
|
||||
}
|
||||
atomic.AddInt64(&nPublished, int64(len(latencies)))
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
sent := atomic.LoadInt64(&nPublished)
|
||||
if sent != nMessages {
|
||||
log.Fatalf("sent %d messages, expected %d", sent, int(nMessages))
|
||||
}
|
||||
}
|
||||
|
||||
func perfClient(pubDelay time.Duration, nConns int, f interface {
|
||||
Fatal(...interface{})
|
||||
}) *pubsub.Client {
|
||||
ctx := context.Background()
|
||||
srv, err := newPerfServer(pubDelay)
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
conn, err := gtransport.DialInsecure(ctx,
|
||||
option.WithEndpoint(srv.Addr),
|
||||
option.WithGRPCConnectionPool(nConns),
|
||||
|
||||
// TODO(grpc/grpc-go#1388) using connection pool without WithBlock
|
||||
// can cause RPCs to fail randomly. We can delete this after the issue is fixed.
|
||||
option.WithGRPCDialOption(grpc.WithBlock()))
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
client, err := pubsub.NewClient(ctx, "projectID", option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
f.Fatal(err)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
type perfServer struct {
|
||||
pb.PublisherServer
|
||||
pb.SubscriberServer
|
||||
|
||||
Addr string
|
||||
pubDelay time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
activePubs int
|
||||
maxActivePubs int
|
||||
}
|
||||
|
||||
func newPerfServer(pubDelay time.Duration) (*perfServer, error) {
|
||||
srv, err := testutil.NewServer(grpc.MaxMsgSize(pubsub.MaxPublishRequestBytes))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
perf := &perfServer{Addr: srv.Addr, pubDelay: pubDelay}
|
||||
pb.RegisterPublisherServer(srv.Gsrv, perf)
|
||||
pb.RegisterSubscriberServer(srv.Gsrv, perf)
|
||||
srv.Start()
|
||||
return perf, nil
|
||||
}
|
||||
|
||||
var doLog = false
|
||||
|
||||
func (p *perfServer) incActivePubs(n int) (int, bool) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.activePubs += n
|
||||
newMax := false
|
||||
if p.activePubs > p.maxActivePubs {
|
||||
p.maxActivePubs = p.activePubs
|
||||
newMax = true
|
||||
}
|
||||
return p.activePubs, newMax
|
||||
}
|
||||
|
||||
func (p *perfServer) Publish(ctx context.Context, req *pb.PublishRequest) (*pb.PublishResponse, error) {
|
||||
a, newMax := p.incActivePubs(1)
|
||||
defer p.incActivePubs(-1)
|
||||
if newMax && doLog {
|
||||
log.Printf("max %d active publish calls", a)
|
||||
}
|
||||
if doLog {
|
||||
log.Printf("%p -> Publish %d", p, len(req.Messages))
|
||||
}
|
||||
res := &pb.PublishResponse{MessageIds: make([]string, len(req.Messages))}
|
||||
for i := range res.MessageIds {
|
||||
res.MessageIds[i] = "x"
|
||||
}
|
||||
time.Sleep(p.pubDelay)
|
||||
if doLog {
|
||||
log.Printf("%p <- Publish %d", p, len(req.Messages))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strconv"
|
||||
|
||||
"math/rand"
|
||||
|
||||
"cloud.google.com/go/pubsub/loadtest"
|
||||
pb "cloud.google.com/go/pubsub/loadtest/pb"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := flag.Uint("worker_port", 6000, "port to bind worker to")
|
||||
role := flag.String("r", "", "role: pub/sub")
|
||||
flag.Parse()
|
||||
|
||||
var lts pb.LoadtestWorkerServer
|
||||
switch *role {
|
||||
case "pub":
|
||||
lts = &loadtest.PubServer{ID: strconv.Itoa(rand.Int())}
|
||||
case "sub":
|
||||
lts = &loadtest.SubServer{}
|
||||
default:
|
||||
log.Fatalf("unknown role: %q", *role)
|
||||
}
|
||||
|
||||
serv := grpc.NewServer()
|
||||
pb.RegisterLoadtestWorkerServer(serv, lts)
|
||||
|
||||
lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
|
||||
if err != nil {
|
||||
log.Fatalf("failed to listen: %v", err)
|
||||
}
|
||||
serv.Serve(lis)
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package loadtest implements load testing for pubsub,
|
||||
// following the interface defined in https://github.com/GoogleCloudPlatform/pubsub/tree/master/load-test-framework/ .
|
||||
//
|
||||
// This package is experimental.
|
||||
package loadtest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
|
||||
"cloud.google.com/go/pubsub"
|
||||
pb "cloud.google.com/go/pubsub/loadtest/pb"
|
||||
)
|
||||
|
||||
type pubServerConfig struct {
|
||||
topic *pubsub.Topic
|
||||
msgData []byte
|
||||
batchSize int32
|
||||
}
|
||||
|
||||
type PubServer struct {
|
||||
ID string
|
||||
|
||||
cfg atomic.Value
|
||||
seqNum int32
|
||||
}
|
||||
|
||||
func (l *PubServer) Start(ctx context.Context, req *pb.StartRequest) (*pb.StartResponse, error) {
|
||||
log.Println("received start")
|
||||
c, err := pubsub.NewClient(ctx, req.Project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dur, err := ptypes.Duration(req.PublishBatchDuration)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
l.init(c, req.Topic, req.MessageSize, req.PublishBatchSize, dur)
|
||||
log.Println("started")
|
||||
return &pb.StartResponse{}, nil
|
||||
}
|
||||
|
||||
func (l *PubServer) init(c *pubsub.Client, topicName string, msgSize, batchSize int32, batchDur time.Duration) {
|
||||
topic := c.Topic(topicName)
|
||||
topic.PublishSettings = pubsub.PublishSettings{
|
||||
DelayThreshold: batchDur,
|
||||
CountThreshold: 950,
|
||||
ByteThreshold: 9500000,
|
||||
}
|
||||
|
||||
l.cfg.Store(pubServerConfig{
|
||||
topic: topic,
|
||||
msgData: bytes.Repeat([]byte{'A'}, int(msgSize)),
|
||||
batchSize: batchSize,
|
||||
})
|
||||
}
|
||||
|
||||
func (l *PubServer) Execute(ctx context.Context, _ *pb.ExecuteRequest) (*pb.ExecuteResponse, error) {
|
||||
latencies, err := l.publishBatch()
|
||||
if err != nil {
|
||||
log.Printf("error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return &pb.ExecuteResponse{Latencies: latencies}, nil
|
||||
}
|
||||
|
||||
func (l *PubServer) publishBatch() ([]int64, error) {
|
||||
var cfg pubServerConfig
|
||||
if c, ok := l.cfg.Load().(pubServerConfig); ok {
|
||||
cfg = c
|
||||
} else {
|
||||
return nil, errors.New("config not loaded")
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
latencies := make([]int64, cfg.batchSize)
|
||||
startStr := strconv.FormatInt(start.UnixNano()/1e6, 10)
|
||||
seqNum := atomic.AddInt32(&l.seqNum, cfg.batchSize) - cfg.batchSize
|
||||
|
||||
rs := make([]*pubsub.PublishResult, cfg.batchSize)
|
||||
for i := int32(0); i < cfg.batchSize; i++ {
|
||||
rs[i] = cfg.topic.Publish(context.TODO(), &pubsub.Message{
|
||||
Data: cfg.msgData,
|
||||
Attributes: map[string]string{
|
||||
"sendTime": startStr,
|
||||
"clientId": l.ID,
|
||||
"sequenceNumber": strconv.Itoa(int(seqNum + i)),
|
||||
},
|
||||
})
|
||||
}
|
||||
for i, r := range rs {
|
||||
_, err := r.Get(context.Background())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO(jba,pongad): fix latencies
|
||||
// Later values will be skewed by earlier ones, since we wait for the
|
||||
// results in order. (On the other hand, it may not matter much, since
|
||||
// messages are added to bundles in order and bundles get sent more or
|
||||
// less in order.) If we want more accurate values, we can either start
|
||||
// a goroutine for each result (similar to the original code using a
|
||||
// callback), or call reflect.Select with the Ready channels of the
|
||||
// results.
|
||||
latencies[i] = time.Since(start).Nanoseconds() / 1e6
|
||||
}
|
||||
return latencies, nil
|
||||
}
|
||||
|
||||
type SubServer struct {
|
||||
lim *rate.Limiter
|
||||
|
||||
mu sync.Mutex
|
||||
idents []*pb.MessageIdentifier
|
||||
latencies []int64
|
||||
}
|
||||
|
||||
func (s *SubServer) Start(ctx context.Context, req *pb.StartRequest) (*pb.StartResponse, error) {
|
||||
log.Println("received start")
|
||||
s.lim = rate.NewLimiter(rate.Every(time.Second), 1)
|
||||
|
||||
c, err := pubsub.NewClient(ctx, req.Project)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Load test API doesn't define any way to stop right now.
|
||||
go func() {
|
||||
sub := c.Subscription(req.GetPubsubOptions().Subscription)
|
||||
sub.ReceiveSettings.NumGoroutines = 10 * runtime.GOMAXPROCS(0)
|
||||
err := sub.Receive(context.Background(), s.callback)
|
||||
log.Fatal(err)
|
||||
}()
|
||||
|
||||
log.Println("started")
|
||||
return &pb.StartResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *SubServer) callback(_ context.Context, m *pubsub.Message) {
|
||||
id, err := strconv.ParseInt(m.Attributes["clientId"], 10, 64)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
m.Nack()
|
||||
return
|
||||
}
|
||||
|
||||
seqNum, err := strconv.ParseInt(m.Attributes["sequenceNumber"], 10, 32)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
m.Nack()
|
||||
return
|
||||
}
|
||||
|
||||
sendTimeMillis, err := strconv.ParseInt(m.Attributes["sendTime"], 10, 64)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
m.Nack()
|
||||
return
|
||||
}
|
||||
|
||||
latency := time.Now().UnixNano()/1e6 - sendTimeMillis
|
||||
ident := &pb.MessageIdentifier{
|
||||
PublisherClientId: id,
|
||||
SequenceNumber: int32(seqNum),
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.idents = append(s.idents, ident)
|
||||
s.latencies = append(s.latencies, latency)
|
||||
s.mu.Unlock()
|
||||
m.Ack()
|
||||
}
|
||||
|
||||
func (s *SubServer) Execute(ctx context.Context, _ *pb.ExecuteRequest) (*pb.ExecuteResponse, error) {
|
||||
// Throttle so the load tester doesn't spam us and consume all our CPU.
|
||||
if err := s.lim.Wait(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
idents := s.idents
|
||||
s.idents = nil
|
||||
latencies := s.latencies
|
||||
s.latencies = nil
|
||||
s.mu.Unlock()
|
||||
|
||||
return &pb.ExecuteResponse{
|
||||
Latencies: latencies,
|
||||
ReceivedMessages: idents,
|
||||
}, nil
|
||||
}
|
||||
+792
@@ -0,0 +1,792 @@
|
||||
// Code generated by protoc-gen-go.
|
||||
// source: loadtest.proto
|
||||
// DO NOT EDIT!
|
||||
|
||||
/*
|
||||
Package google_pubsub_loadtest is a generated protocol buffer package.
|
||||
|
||||
It is generated from these files:
|
||||
loadtest.proto
|
||||
|
||||
It has these top-level messages:
|
||||
StartRequest
|
||||
StartResponse
|
||||
PubsubOptions
|
||||
KafkaOptions
|
||||
MessageIdentifier
|
||||
CheckRequest
|
||||
CheckResponse
|
||||
ExecuteRequest
|
||||
ExecuteResponse
|
||||
*/
|
||||
package google_pubsub_loadtest
|
||||
|
||||
import proto "github.com/golang/protobuf/proto"
|
||||
import fmt "fmt"
|
||||
import math "math"
|
||||
import google_protobuf "github.com/golang/protobuf/ptypes/duration"
|
||||
import google_protobuf1 "github.com/golang/protobuf/ptypes/timestamp"
|
||||
|
||||
import (
|
||||
context "golang.org/x/net/context"
|
||||
grpc "google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ = proto.Marshal
|
||||
var _ = fmt.Errorf
|
||||
var _ = math.Inf
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the proto package it is being compiled against.
|
||||
// A compilation error at this line likely means your copy of the
|
||||
// proto package needs to be updated.
|
||||
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||
|
||||
type StartRequest struct {
|
||||
// The GCP project. This must be set even for Kafka, as we use it to export metrics.
|
||||
Project string `protobuf:"bytes,1,opt,name=project" json:"project,omitempty"`
|
||||
// The Pub/Sub or Kafka topic name.
|
||||
Topic string `protobuf:"bytes,2,opt,name=topic" json:"topic,omitempty"`
|
||||
// The number of requests that can be made, each second, per client.
|
||||
RequestRate int32 `protobuf:"varint,3,opt,name=request_rate,json=requestRate" json:"request_rate,omitempty"`
|
||||
// The size of each user message to publish
|
||||
MessageSize int32 `protobuf:"varint,4,opt,name=message_size,json=messageSize" json:"message_size,omitempty"`
|
||||
// The maximum outstanding requests, per client.
|
||||
MaxOutstandingRequests int32 `protobuf:"varint,5,opt,name=max_outstanding_requests,json=maxOutstandingRequests" json:"max_outstanding_requests,omitempty"`
|
||||
// The time at which the load test should start. If this is less than the current time, we start immediately.
|
||||
StartTime *google_protobuf1.Timestamp `protobuf:"bytes,6,opt,name=start_time,json=startTime" json:"start_time,omitempty"`
|
||||
// The burn-in duration, before which results should not be reported.
|
||||
BurnInDuration *google_protobuf.Duration `protobuf:"bytes,12,opt,name=burn_in_duration,json=burnInDuration" json:"burn_in_duration,omitempty"`
|
||||
// The number of user messages of size message_size to publish together.
|
||||
PublishBatchSize int32 `protobuf:"varint,11,opt,name=publish_batch_size,json=publishBatchSize" json:"publish_batch_size,omitempty"`
|
||||
// The max duration for coalescing a batch of published messages.
|
||||
PublishBatchDuration *google_protobuf.Duration `protobuf:"bytes,13,opt,name=publish_batch_duration,json=publishBatchDuration" json:"publish_batch_duration,omitempty"`
|
||||
// Types that are valid to be assigned to StopConditions:
|
||||
// *StartRequest_TestDuration
|
||||
// *StartRequest_NumberOfMessages
|
||||
StopConditions isStartRequest_StopConditions `protobuf_oneof:"stop_conditions"`
|
||||
// Types that are valid to be assigned to Options:
|
||||
// *StartRequest_PubsubOptions
|
||||
// *StartRequest_KafkaOptions
|
||||
Options isStartRequest_Options `protobuf_oneof:"options"`
|
||||
}
|
||||
|
||||
func (m *StartRequest) Reset() { *m = StartRequest{} }
|
||||
func (m *StartRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*StartRequest) ProtoMessage() {}
|
||||
func (*StartRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
|
||||
|
||||
type isStartRequest_StopConditions interface {
|
||||
isStartRequest_StopConditions()
|
||||
}
|
||||
type isStartRequest_Options interface {
|
||||
isStartRequest_Options()
|
||||
}
|
||||
|
||||
type StartRequest_TestDuration struct {
|
||||
TestDuration *google_protobuf.Duration `protobuf:"bytes,7,opt,name=test_duration,json=testDuration,oneof"`
|
||||
}
|
||||
type StartRequest_NumberOfMessages struct {
|
||||
NumberOfMessages int32 `protobuf:"varint,8,opt,name=number_of_messages,json=numberOfMessages,oneof"`
|
||||
}
|
||||
type StartRequest_PubsubOptions struct {
|
||||
PubsubOptions *PubsubOptions `protobuf:"bytes,9,opt,name=pubsub_options,json=pubsubOptions,oneof"`
|
||||
}
|
||||
type StartRequest_KafkaOptions struct {
|
||||
KafkaOptions *KafkaOptions `protobuf:"bytes,10,opt,name=kafka_options,json=kafkaOptions,oneof"`
|
||||
}
|
||||
|
||||
func (*StartRequest_TestDuration) isStartRequest_StopConditions() {}
|
||||
func (*StartRequest_NumberOfMessages) isStartRequest_StopConditions() {}
|
||||
func (*StartRequest_PubsubOptions) isStartRequest_Options() {}
|
||||
func (*StartRequest_KafkaOptions) isStartRequest_Options() {}
|
||||
|
||||
func (m *StartRequest) GetStopConditions() isStartRequest_StopConditions {
|
||||
if m != nil {
|
||||
return m.StopConditions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (m *StartRequest) GetOptions() isStartRequest_Options {
|
||||
if m != nil {
|
||||
return m.Options
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetProject() string {
|
||||
if m != nil {
|
||||
return m.Project
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetTopic() string {
|
||||
if m != nil {
|
||||
return m.Topic
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetRequestRate() int32 {
|
||||
if m != nil {
|
||||
return m.RequestRate
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetMessageSize() int32 {
|
||||
if m != nil {
|
||||
return m.MessageSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetMaxOutstandingRequests() int32 {
|
||||
if m != nil {
|
||||
return m.MaxOutstandingRequests
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetStartTime() *google_protobuf1.Timestamp {
|
||||
if m != nil {
|
||||
return m.StartTime
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetBurnInDuration() *google_protobuf.Duration {
|
||||
if m != nil {
|
||||
return m.BurnInDuration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetPublishBatchSize() int32 {
|
||||
if m != nil {
|
||||
return m.PublishBatchSize
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetPublishBatchDuration() *google_protobuf.Duration {
|
||||
if m != nil {
|
||||
return m.PublishBatchDuration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetTestDuration() *google_protobuf.Duration {
|
||||
if x, ok := m.GetStopConditions().(*StartRequest_TestDuration); ok {
|
||||
return x.TestDuration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetNumberOfMessages() int32 {
|
||||
if x, ok := m.GetStopConditions().(*StartRequest_NumberOfMessages); ok {
|
||||
return x.NumberOfMessages
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetPubsubOptions() *PubsubOptions {
|
||||
if x, ok := m.GetOptions().(*StartRequest_PubsubOptions); ok {
|
||||
return x.PubsubOptions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *StartRequest) GetKafkaOptions() *KafkaOptions {
|
||||
if x, ok := m.GetOptions().(*StartRequest_KafkaOptions); ok {
|
||||
return x.KafkaOptions
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// XXX_OneofFuncs is for the internal use of the proto package.
|
||||
func (*StartRequest) XXX_OneofFuncs() (func(msg proto.Message, b *proto.Buffer) error, func(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error), func(msg proto.Message) (n int), []interface{}) {
|
||||
return _StartRequest_OneofMarshaler, _StartRequest_OneofUnmarshaler, _StartRequest_OneofSizer, []interface{}{
|
||||
(*StartRequest_TestDuration)(nil),
|
||||
(*StartRequest_NumberOfMessages)(nil),
|
||||
(*StartRequest_PubsubOptions)(nil),
|
||||
(*StartRequest_KafkaOptions)(nil),
|
||||
}
|
||||
}
|
||||
|
||||
func _StartRequest_OneofMarshaler(msg proto.Message, b *proto.Buffer) error {
|
||||
m := msg.(*StartRequest)
|
||||
// stop_conditions
|
||||
switch x := m.StopConditions.(type) {
|
||||
case *StartRequest_TestDuration:
|
||||
b.EncodeVarint(7<<3 | proto.WireBytes)
|
||||
if err := b.EncodeMessage(x.TestDuration); err != nil {
|
||||
return err
|
||||
}
|
||||
case *StartRequest_NumberOfMessages:
|
||||
b.EncodeVarint(8<<3 | proto.WireVarint)
|
||||
b.EncodeVarint(uint64(x.NumberOfMessages))
|
||||
case nil:
|
||||
default:
|
||||
return fmt.Errorf("StartRequest.StopConditions has unexpected type %T", x)
|
||||
}
|
||||
// options
|
||||
switch x := m.Options.(type) {
|
||||
case *StartRequest_PubsubOptions:
|
||||
b.EncodeVarint(9<<3 | proto.WireBytes)
|
||||
if err := b.EncodeMessage(x.PubsubOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
case *StartRequest_KafkaOptions:
|
||||
b.EncodeVarint(10<<3 | proto.WireBytes)
|
||||
if err := b.EncodeMessage(x.KafkaOptions); err != nil {
|
||||
return err
|
||||
}
|
||||
case nil:
|
||||
default:
|
||||
return fmt.Errorf("StartRequest.Options has unexpected type %T", x)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _StartRequest_OneofUnmarshaler(msg proto.Message, tag, wire int, b *proto.Buffer) (bool, error) {
|
||||
m := msg.(*StartRequest)
|
||||
switch tag {
|
||||
case 7: // stop_conditions.test_duration
|
||||
if wire != proto.WireBytes {
|
||||
return true, proto.ErrInternalBadWireType
|
||||
}
|
||||
msg := new(google_protobuf.Duration)
|
||||
err := b.DecodeMessage(msg)
|
||||
m.StopConditions = &StartRequest_TestDuration{msg}
|
||||
return true, err
|
||||
case 8: // stop_conditions.number_of_messages
|
||||
if wire != proto.WireVarint {
|
||||
return true, proto.ErrInternalBadWireType
|
||||
}
|
||||
x, err := b.DecodeVarint()
|
||||
m.StopConditions = &StartRequest_NumberOfMessages{int32(x)}
|
||||
return true, err
|
||||
case 9: // options.pubsub_options
|
||||
if wire != proto.WireBytes {
|
||||
return true, proto.ErrInternalBadWireType
|
||||
}
|
||||
msg := new(PubsubOptions)
|
||||
err := b.DecodeMessage(msg)
|
||||
m.Options = &StartRequest_PubsubOptions{msg}
|
||||
return true, err
|
||||
case 10: // options.kafka_options
|
||||
if wire != proto.WireBytes {
|
||||
return true, proto.ErrInternalBadWireType
|
||||
}
|
||||
msg := new(KafkaOptions)
|
||||
err := b.DecodeMessage(msg)
|
||||
m.Options = &StartRequest_KafkaOptions{msg}
|
||||
return true, err
|
||||
default:
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func _StartRequest_OneofSizer(msg proto.Message) (n int) {
|
||||
m := msg.(*StartRequest)
|
||||
// stop_conditions
|
||||
switch x := m.StopConditions.(type) {
|
||||
case *StartRequest_TestDuration:
|
||||
s := proto.Size(x.TestDuration)
|
||||
n += proto.SizeVarint(7<<3 | proto.WireBytes)
|
||||
n += proto.SizeVarint(uint64(s))
|
||||
n += s
|
||||
case *StartRequest_NumberOfMessages:
|
||||
n += proto.SizeVarint(8<<3 | proto.WireVarint)
|
||||
n += proto.SizeVarint(uint64(x.NumberOfMessages))
|
||||
case nil:
|
||||
default:
|
||||
panic(fmt.Sprintf("proto: unexpected type %T in oneof", x))
|
||||
}
|
||||
// options
|
||||
switch x := m.Options.(type) {
|
||||
case *StartRequest_PubsubOptions:
|
||||
s := proto.Size(x.PubsubOptions)
|
||||
n += proto.SizeVarint(9<<3 | proto.WireBytes)
|
||||
n += proto.SizeVarint(uint64(s))
|
||||
n += s
|
||||
case *StartRequest_KafkaOptions:
|
||||
s := proto.Size(x.KafkaOptions)
|
||||
n += proto.SizeVarint(10<<3 | proto.WireBytes)
|
||||
n += proto.SizeVarint(uint64(s))
|
||||
n += s
|
||||
case nil:
|
||||
default:
|
||||
panic(fmt.Sprintf("proto: unexpected type %T in oneof", x))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type StartResponse struct {
|
||||
}
|
||||
|
||||
func (m *StartResponse) Reset() { *m = StartResponse{} }
|
||||
func (m *StartResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*StartResponse) ProtoMessage() {}
|
||||
func (*StartResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
|
||||
|
||||
type PubsubOptions struct {
|
||||
// The Cloud Pub/Sub subscription name
|
||||
Subscription string `protobuf:"bytes,1,opt,name=subscription" json:"subscription,omitempty"`
|
||||
// The maximum number of messages to pull which each request.
|
||||
MaxMessagesPerPull int32 `protobuf:"varint,2,opt,name=max_messages_per_pull,json=maxMessagesPerPull" json:"max_messages_per_pull,omitempty"`
|
||||
}
|
||||
|
||||
func (m *PubsubOptions) Reset() { *m = PubsubOptions{} }
|
||||
func (m *PubsubOptions) String() string { return proto.CompactTextString(m) }
|
||||
func (*PubsubOptions) ProtoMessage() {}
|
||||
func (*PubsubOptions) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
|
||||
|
||||
func (m *PubsubOptions) GetSubscription() string {
|
||||
if m != nil {
|
||||
return m.Subscription
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *PubsubOptions) GetMaxMessagesPerPull() int32 {
|
||||
if m != nil {
|
||||
return m.MaxMessagesPerPull
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type KafkaOptions struct {
|
||||
// The network address of the Kafka broker.
|
||||
Broker string `protobuf:"bytes,1,opt,name=broker" json:"broker,omitempty"`
|
||||
// The length of time to poll for.
|
||||
PollDuration *google_protobuf.Duration `protobuf:"bytes,2,opt,name=poll_duration,json=pollDuration" json:"poll_duration,omitempty"`
|
||||
}
|
||||
|
||||
func (m *KafkaOptions) Reset() { *m = KafkaOptions{} }
|
||||
func (m *KafkaOptions) String() string { return proto.CompactTextString(m) }
|
||||
func (*KafkaOptions) ProtoMessage() {}
|
||||
func (*KafkaOptions) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
|
||||
|
||||
func (m *KafkaOptions) GetBroker() string {
|
||||
if m != nil {
|
||||
return m.Broker
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m *KafkaOptions) GetPollDuration() *google_protobuf.Duration {
|
||||
if m != nil {
|
||||
return m.PollDuration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type MessageIdentifier struct {
|
||||
// The unique id of the client that published the message.
|
||||
PublisherClientId int64 `protobuf:"varint,1,opt,name=publisher_client_id,json=publisherClientId" json:"publisher_client_id,omitempty"`
|
||||
// Sequence number of the published message with the given publish_client_id.
|
||||
SequenceNumber int32 `protobuf:"varint,2,opt,name=sequence_number,json=sequenceNumber" json:"sequence_number,omitempty"`
|
||||
}
|
||||
|
||||
func (m *MessageIdentifier) Reset() { *m = MessageIdentifier{} }
|
||||
func (m *MessageIdentifier) String() string { return proto.CompactTextString(m) }
|
||||
func (*MessageIdentifier) ProtoMessage() {}
|
||||
func (*MessageIdentifier) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
|
||||
|
||||
func (m *MessageIdentifier) GetPublisherClientId() int64 {
|
||||
if m != nil {
|
||||
return m.PublisherClientId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *MessageIdentifier) GetSequenceNumber() int32 {
|
||||
if m != nil {
|
||||
return m.SequenceNumber
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type CheckRequest struct {
|
||||
// Duplicate messages that should not be reported for throughput and latency.
|
||||
Duplicates []*MessageIdentifier `protobuf:"bytes,1,rep,name=duplicates" json:"duplicates,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CheckRequest) Reset() { *m = CheckRequest{} }
|
||||
func (m *CheckRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*CheckRequest) ProtoMessage() {}
|
||||
func (*CheckRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
|
||||
|
||||
func (m *CheckRequest) GetDuplicates() []*MessageIdentifier {
|
||||
if m != nil {
|
||||
return m.Duplicates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type CheckResponse struct {
|
||||
// Histogram of latencies, each one a delta from the previous CheckResponse sent.
|
||||
BucketValues []int64 `protobuf:"varint,1,rep,packed,name=bucket_values,json=bucketValues" json:"bucket_values,omitempty"`
|
||||
// The duration from the start of the loadtest to its completion or now if is_finished is false.
|
||||
RunningDuration *google_protobuf.Duration `protobuf:"bytes,2,opt,name=running_duration,json=runningDuration" json:"running_duration,omitempty"`
|
||||
// True if the load test has finished running.
|
||||
IsFinished bool `protobuf:"varint,3,opt,name=is_finished,json=isFinished" json:"is_finished,omitempty"`
|
||||
// MessageIdentifiers of all received messages since the last Check
|
||||
ReceivedMessages []*MessageIdentifier `protobuf:"bytes,4,rep,name=received_messages,json=receivedMessages" json:"received_messages,omitempty"`
|
||||
}
|
||||
|
||||
func (m *CheckResponse) Reset() { *m = CheckResponse{} }
|
||||
func (m *CheckResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*CheckResponse) ProtoMessage() {}
|
||||
func (*CheckResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
|
||||
|
||||
func (m *CheckResponse) GetBucketValues() []int64 {
|
||||
if m != nil {
|
||||
return m.BucketValues
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CheckResponse) GetRunningDuration() *google_protobuf.Duration {
|
||||
if m != nil {
|
||||
return m.RunningDuration
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *CheckResponse) GetIsFinished() bool {
|
||||
if m != nil {
|
||||
return m.IsFinished
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *CheckResponse) GetReceivedMessages() []*MessageIdentifier {
|
||||
if m != nil {
|
||||
return m.ReceivedMessages
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ExecuteRequest struct {
|
||||
}
|
||||
|
||||
func (m *ExecuteRequest) Reset() { *m = ExecuteRequest{} }
|
||||
func (m *ExecuteRequest) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecuteRequest) ProtoMessage() {}
|
||||
func (*ExecuteRequest) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} }
|
||||
|
||||
type ExecuteResponse struct {
|
||||
// Latencies of the completed operations
|
||||
Latencies []int64 `protobuf:"varint,1,rep,packed,name=latencies" json:"latencies,omitempty"`
|
||||
// MessageIdentifiers of all received messages since the last Execute
|
||||
ReceivedMessages []*MessageIdentifier `protobuf:"bytes,2,rep,name=received_messages,json=receivedMessages" json:"received_messages,omitempty"`
|
||||
}
|
||||
|
||||
func (m *ExecuteResponse) Reset() { *m = ExecuteResponse{} }
|
||||
func (m *ExecuteResponse) String() string { return proto.CompactTextString(m) }
|
||||
func (*ExecuteResponse) ProtoMessage() {}
|
||||
func (*ExecuteResponse) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} }
|
||||
|
||||
func (m *ExecuteResponse) GetLatencies() []int64 {
|
||||
if m != nil {
|
||||
return m.Latencies
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ExecuteResponse) GetReceivedMessages() []*MessageIdentifier {
|
||||
if m != nil {
|
||||
return m.ReceivedMessages
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
proto.RegisterType((*StartRequest)(nil), "google.pubsub.loadtest.StartRequest")
|
||||
proto.RegisterType((*StartResponse)(nil), "google.pubsub.loadtest.StartResponse")
|
||||
proto.RegisterType((*PubsubOptions)(nil), "google.pubsub.loadtest.PubsubOptions")
|
||||
proto.RegisterType((*KafkaOptions)(nil), "google.pubsub.loadtest.KafkaOptions")
|
||||
proto.RegisterType((*MessageIdentifier)(nil), "google.pubsub.loadtest.MessageIdentifier")
|
||||
proto.RegisterType((*CheckRequest)(nil), "google.pubsub.loadtest.CheckRequest")
|
||||
proto.RegisterType((*CheckResponse)(nil), "google.pubsub.loadtest.CheckResponse")
|
||||
proto.RegisterType((*ExecuteRequest)(nil), "google.pubsub.loadtest.ExecuteRequest")
|
||||
proto.RegisterType((*ExecuteResponse)(nil), "google.pubsub.loadtest.ExecuteResponse")
|
||||
}
|
||||
|
||||
// Reference imports to suppress errors if they are not otherwise used.
|
||||
var _ context.Context
|
||||
var _ grpc.ClientConn
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
const _ = grpc.SupportPackageIsVersion4
|
||||
|
||||
// Client API for Loadtest service
|
||||
|
||||
type LoadtestClient interface {
|
||||
// Starts a load test
|
||||
Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption) (*StartResponse, error)
|
||||
// Checks the status of a load test
|
||||
Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error)
|
||||
}
|
||||
|
||||
type loadtestClient struct {
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func NewLoadtestClient(cc *grpc.ClientConn) LoadtestClient {
|
||||
return &loadtestClient{cc}
|
||||
}
|
||||
|
||||
func (c *loadtestClient) Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption) (*StartResponse, error) {
|
||||
out := new(StartResponse)
|
||||
err := grpc.Invoke(ctx, "/google.pubsub.loadtest.Loadtest/Start", in, out, c.cc, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *loadtestClient) Check(ctx context.Context, in *CheckRequest, opts ...grpc.CallOption) (*CheckResponse, error) {
|
||||
out := new(CheckResponse)
|
||||
err := grpc.Invoke(ctx, "/google.pubsub.loadtest.Loadtest/Check", in, out, c.cc, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for Loadtest service
|
||||
|
||||
type LoadtestServer interface {
|
||||
// Starts a load test
|
||||
Start(context.Context, *StartRequest) (*StartResponse, error)
|
||||
// Checks the status of a load test
|
||||
Check(context.Context, *CheckRequest) (*CheckResponse, error)
|
||||
}
|
||||
|
||||
func RegisterLoadtestServer(s *grpc.Server, srv LoadtestServer) {
|
||||
s.RegisterService(&_Loadtest_serviceDesc, srv)
|
||||
}
|
||||
|
||||
func _Loadtest_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StartRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LoadtestServer).Start(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/google.pubsub.loadtest.Loadtest/Start",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LoadtestServer).Start(ctx, req.(*StartRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Loadtest_Check_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(CheckRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LoadtestServer).Check(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/google.pubsub.loadtest.Loadtest/Check",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LoadtestServer).Check(ctx, req.(*CheckRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _Loadtest_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "google.pubsub.loadtest.Loadtest",
|
||||
HandlerType: (*LoadtestServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Start",
|
||||
Handler: _Loadtest_Start_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Check",
|
||||
Handler: _Loadtest_Check_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "loadtest.proto",
|
||||
}
|
||||
|
||||
// Client API for LoadtestWorker service
|
||||
|
||||
type LoadtestWorkerClient interface {
|
||||
// Starts a worker
|
||||
Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption) (*StartResponse, error)
|
||||
// Executes a command on the worker, returning the latencies of the operations. Since some
|
||||
// commands consist of multiple operations (i.e. pulls contain many received messages with
|
||||
// different end to end latencies) a single command can have multiple latencies returned.
|
||||
Execute(ctx context.Context, in *ExecuteRequest, opts ...grpc.CallOption) (*ExecuteResponse, error)
|
||||
}
|
||||
|
||||
type loadtestWorkerClient struct {
|
||||
cc *grpc.ClientConn
|
||||
}
|
||||
|
||||
func NewLoadtestWorkerClient(cc *grpc.ClientConn) LoadtestWorkerClient {
|
||||
return &loadtestWorkerClient{cc}
|
||||
}
|
||||
|
||||
func (c *loadtestWorkerClient) Start(ctx context.Context, in *StartRequest, opts ...grpc.CallOption) (*StartResponse, error) {
|
||||
out := new(StartResponse)
|
||||
err := grpc.Invoke(ctx, "/google.pubsub.loadtest.LoadtestWorker/Start", in, out, c.cc, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *loadtestWorkerClient) Execute(ctx context.Context, in *ExecuteRequest, opts ...grpc.CallOption) (*ExecuteResponse, error) {
|
||||
out := new(ExecuteResponse)
|
||||
err := grpc.Invoke(ctx, "/google.pubsub.loadtest.LoadtestWorker/Execute", in, out, c.cc, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Server API for LoadtestWorker service
|
||||
|
||||
type LoadtestWorkerServer interface {
|
||||
// Starts a worker
|
||||
Start(context.Context, *StartRequest) (*StartResponse, error)
|
||||
// Executes a command on the worker, returning the latencies of the operations. Since some
|
||||
// commands consist of multiple operations (i.e. pulls contain many received messages with
|
||||
// different end to end latencies) a single command can have multiple latencies returned.
|
||||
Execute(context.Context, *ExecuteRequest) (*ExecuteResponse, error)
|
||||
}
|
||||
|
||||
func RegisterLoadtestWorkerServer(s *grpc.Server, srv LoadtestWorkerServer) {
|
||||
s.RegisterService(&_LoadtestWorker_serviceDesc, srv)
|
||||
}
|
||||
|
||||
func _LoadtestWorker_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(StartRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LoadtestWorkerServer).Start(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/google.pubsub.loadtest.LoadtestWorker/Start",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LoadtestWorkerServer).Start(ctx, req.(*StartRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _LoadtestWorker_Execute_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ExecuteRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(LoadtestWorkerServer).Execute(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/google.pubsub.loadtest.LoadtestWorker/Execute",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(LoadtestWorkerServer).Execute(ctx, req.(*ExecuteRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
var _LoadtestWorker_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "google.pubsub.loadtest.LoadtestWorker",
|
||||
HandlerType: (*LoadtestWorkerServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Start",
|
||||
Handler: _LoadtestWorker_Start_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Execute",
|
||||
Handler: _LoadtestWorker_Execute_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "loadtest.proto",
|
||||
}
|
||||
|
||||
func init() { proto.RegisterFile("loadtest.proto", fileDescriptor0) }
|
||||
|
||||
var fileDescriptor0 = []byte{
|
||||
// 847 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x55, 0xdd, 0x6e, 0xdc, 0x44,
|
||||
0x14, 0xae, 0x93, 0x6e, 0x92, 0x3d, 0x6b, 0xef, 0x6e, 0x86, 0x12, 0x99, 0x15, 0xd0, 0x60, 0x28,
|
||||
0x0d, 0x12, 0x72, 0x45, 0xb8, 0x81, 0x1b, 0x84, 0x92, 0x82, 0x12, 0x15, 0x9a, 0xc8, 0x8d, 0x8a,
|
||||
0xe0, 0x66, 0x34, 0xb6, 0x67, 0x93, 0x61, 0xed, 0x19, 0x33, 0x3f, 0x55, 0xd4, 0x17, 0xe0, 0x8d,
|
||||
0x78, 0x00, 0x1e, 0x87, 0x5b, 0x5e, 0x00, 0xcd, 0x78, 0xbc, 0x3f, 0x6d, 0x57, 0x0b, 0x42, 0xbd,
|
||||
0x3c, 0xdf, 0xf9, 0xce, 0x37, 0xe7, 0xd7, 0x86, 0x61, 0x25, 0x48, 0xa9, 0xa9, 0xd2, 0x69, 0x23,
|
||||
0x85, 0x16, 0xe8, 0xe0, 0x5a, 0x88, 0xeb, 0x8a, 0xa6, 0x8d, 0xc9, 0x95, 0xc9, 0xd3, 0xce, 0x3b,
|
||||
0xf9, 0xb0, 0xc5, 0x1f, 0x39, 0x56, 0x6e, 0xa6, 0x8f, 0x4a, 0x23, 0x89, 0x66, 0x82, 0xb7, 0x71,
|
||||
0x93, 0xfb, 0xaf, 0xfa, 0x35, 0xab, 0xa9, 0xd2, 0xa4, 0x6e, 0x5a, 0x42, 0xf2, 0x57, 0x0f, 0xc2,
|
||||
0x67, 0x9a, 0x48, 0x9d, 0xd1, 0xdf, 0x0c, 0x55, 0x1a, 0xc5, 0xb0, 0xdb, 0x48, 0xf1, 0x2b, 0x2d,
|
||||
0x74, 0x1c, 0x1c, 0x06, 0x47, 0xfd, 0xac, 0x33, 0xd1, 0x3d, 0xe8, 0x69, 0xd1, 0xb0, 0x22, 0xde,
|
||||
0x72, 0x78, 0x6b, 0xa0, 0x8f, 0x20, 0x94, 0x6d, 0x28, 0x96, 0x44, 0xd3, 0x78, 0xfb, 0x30, 0x38,
|
||||
0xea, 0x65, 0x03, 0x8f, 0x65, 0x44, 0x53, 0x4b, 0xa9, 0xa9, 0x52, 0xe4, 0x9a, 0x62, 0xc5, 0x5e,
|
||||
0xd2, 0xf8, 0x6e, 0x4b, 0xf1, 0xd8, 0x33, 0xf6, 0x92, 0xa2, 0xaf, 0x20, 0xae, 0xc9, 0x2d, 0x16,
|
||||
0x46, 0x2b, 0x4d, 0x78, 0xc9, 0xf8, 0x35, 0xf6, 0x0a, 0x2a, 0xee, 0x39, 0xfa, 0x41, 0x4d, 0x6e,
|
||||
0x2f, 0x16, 0x6e, 0x9f, 0xae, 0x42, 0x5f, 0x03, 0x28, 0x9b, 0x3f, 0xb6, 0x95, 0xc5, 0x3b, 0x87,
|
||||
0xc1, 0xd1, 0xe0, 0x78, 0x92, 0x76, 0xed, 0xf2, 0x65, 0xa7, 0x57, 0x5d, 0xd9, 0x59, 0xdf, 0xb1,
|
||||
0xad, 0x8d, 0x4e, 0x61, 0x9c, 0x1b, 0xc9, 0x31, 0xe3, 0xb8, 0x6b, 0x5b, 0x1c, 0x3a, 0x81, 0xf7,
|
||||
0x5e, 0x13, 0x78, 0xec, 0x09, 0xd9, 0xd0, 0x86, 0x9c, 0xf3, 0xce, 0x46, 0x9f, 0x03, 0x6a, 0x4c,
|
||||
0x5e, 0x31, 0x75, 0x83, 0x73, 0xa2, 0x8b, 0x9b, 0xb6, 0xc4, 0x81, 0xcb, 0x79, 0xec, 0x3d, 0x27,
|
||||
0xd6, 0xe1, 0xea, 0xbc, 0x80, 0x83, 0x55, 0xf6, 0xfc, 0xe1, 0x68, 0xd3, 0xc3, 0xf7, 0x96, 0xc5,
|
||||
0xe6, 0xcf, 0x7f, 0x0b, 0x91, 0x5d, 0x84, 0x85, 0xce, 0xee, 0x06, 0x9d, 0xb3, 0x3b, 0x59, 0x68,
|
||||
0x23, 0xe6, 0x0a, 0x29, 0x20, 0x6e, 0xea, 0x9c, 0x4a, 0x2c, 0xa6, 0xd8, 0xcf, 0x44, 0xc5, 0x7b,
|
||||
0xb6, 0x80, 0xb3, 0x3b, 0xd9, 0xb8, 0xf5, 0x5d, 0x4c, 0x7f, 0xf4, 0x1e, 0xf4, 0x14, 0x86, 0xed,
|
||||
0x16, 0x62, 0xd1, 0x58, 0x01, 0x15, 0xf7, 0xdd, 0x93, 0x0f, 0xd2, 0x37, 0xef, 0x68, 0x7a, 0xe9,
|
||||
0xec, 0x8b, 0x96, 0x7c, 0x16, 0x64, 0x51, 0xb3, 0x0c, 0xa0, 0x27, 0x10, 0xcd, 0xc8, 0x74, 0x46,
|
||||
0xe6, 0x72, 0xe0, 0xe4, 0x3e, 0x59, 0x27, 0xf7, 0xc4, 0x92, 0x17, 0x6a, 0xe1, 0x6c, 0xc9, 0x3e,
|
||||
0xd9, 0x87, 0x91, 0xd2, 0xa2, 0xc1, 0x85, 0xe0, 0x25, 0x6b, 0xa1, 0x3e, 0xec, 0x7a, 0xe5, 0x64,
|
||||
0x04, 0x91, 0xdf, 0x75, 0xd5, 0x08, 0xae, 0x68, 0x32, 0x85, 0x68, 0x25, 0x3b, 0x94, 0x40, 0xa8,
|
||||
0x4c, 0xae, 0x0a, 0xc9, 0x1c, 0xe0, 0x4f, 0x60, 0x05, 0x43, 0x5f, 0xc0, 0xbb, 0x76, 0x57, 0xbb,
|
||||
0x56, 0xe1, 0x86, 0x4a, 0xdc, 0x98, 0xaa, 0x72, 0x77, 0xd1, 0xcb, 0x50, 0x4d, 0x6e, 0xbb, 0x66,
|
||||
0x5d, 0x52, 0x79, 0x69, 0xaa, 0x2a, 0x99, 0x42, 0xb8, 0x9c, 0x36, 0x3a, 0x80, 0x9d, 0x5c, 0x8a,
|
||||
0x19, 0x95, 0xfe, 0x01, 0x6f, 0xa1, 0x6f, 0x20, 0x6a, 0x44, 0x55, 0x2d, 0xa6, 0xb9, 0xb5, 0x69,
|
||||
0x2b, 0x42, 0xcb, 0xef, 0xac, 0xa4, 0x82, 0x7d, 0xff, 0xf4, 0x79, 0x49, 0xb9, 0x66, 0x53, 0x46,
|
||||
0x25, 0x4a, 0xe1, 0x1d, 0xbf, 0x3a, 0x54, 0xe2, 0xa2, 0x62, 0x94, 0x6b, 0xcc, 0x4a, 0xf7, 0xf2,
|
||||
0x76, 0xb6, 0x3f, 0x77, 0x9d, 0x3a, 0xcf, 0x79, 0x89, 0x1e, 0xc2, 0x48, 0xd9, 0xeb, 0xe2, 0x05,
|
||||
0xc5, 0xed, 0xf4, 0x7d, 0x65, 0xc3, 0x0e, 0x7e, 0xea, 0xd0, 0xe4, 0x67, 0x08, 0x4f, 0x6f, 0x68,
|
||||
0x31, 0xeb, 0x3e, 0x1d, 0xe7, 0x00, 0xa5, 0x69, 0x2a, 0x56, 0x10, 0x4d, 0x55, 0x1c, 0x1c, 0x6e,
|
||||
0x1f, 0x0d, 0x8e, 0x3f, 0x5b, 0x37, 0xc6, 0xd7, 0xf2, 0xcc, 0x96, 0x82, 0x93, 0xbf, 0x03, 0x88,
|
||||
0xbc, 0x76, 0x3b, 0x2a, 0xf4, 0x31, 0x44, 0xb9, 0x29, 0x66, 0x54, 0xe3, 0x17, 0xa4, 0x32, 0x5e,
|
||||
0x7f, 0x3b, 0x0b, 0x5b, 0xf0, 0xb9, 0xc3, 0xd0, 0x63, 0x18, 0x4b, 0xc3, 0xb9, 0xfd, 0x7c, 0xfc,
|
||||
0xfb, 0x16, 0x8e, 0x7c, 0xc8, 0xfc, 0x22, 0xee, 0xc3, 0x80, 0x29, 0x3c, 0x65, 0xdc, 0xf6, 0xa5,
|
||||
0x74, 0x5f, 0xb4, 0xbd, 0x0c, 0x98, 0xfa, 0xde, 0x23, 0xe8, 0x39, 0xec, 0x4b, 0x5a, 0x50, 0xf6,
|
||||
0x82, 0x96, 0x8b, 0x8b, 0xb9, 0xfb, 0x5f, 0xeb, 0x1d, 0x77, 0x1a, 0xdd, 0xb6, 0x24, 0x63, 0x18,
|
||||
0x7e, 0x77, 0x4b, 0x0b, 0xa3, 0xa9, 0x6f, 0x69, 0xf2, 0x7b, 0x00, 0xa3, 0x39, 0xe4, 0x3b, 0xf1,
|
||||
0x3e, 0xf4, 0x2b, 0xa2, 0x29, 0x2f, 0xd8, 0xbc, 0x0b, 0x0b, 0xe0, 0xcd, 0xb9, 0x6d, 0xfd, 0xef,
|
||||
0xdc, 0x8e, 0xff, 0x08, 0x60, 0xef, 0x07, 0x1f, 0x80, 0xae, 0xa0, 0xe7, 0x0e, 0x09, 0xad, 0xbd,
|
||||
0xd2, 0xe5, 0x7f, 0xca, 0xe4, 0xc1, 0x06, 0x96, 0x2f, 0xec, 0x0a, 0x7a, 0x6e, 0xe6, 0xeb, 0x55,
|
||||
0x97, 0xd7, 0x6d, 0xbd, 0xea, 0xca, 0xe2, 0x1c, 0xff, 0x19, 0xc0, 0xb0, 0x4b, 0xfc, 0x27, 0x21,
|
||||
0xed, 0x99, 0xbd, 0x9d, 0xf4, 0x7f, 0x81, 0x5d, 0x3f, 0x2a, 0xf4, 0xe9, 0xba, 0x88, 0xd5, 0xf1,
|
||||
0x4e, 0x1e, 0x6e, 0xe4, 0xb5, 0xda, 0x27, 0x29, 0x7c, 0x50, 0x88, 0xfa, 0x15, 0xf6, 0xb4, 0x62,
|
||||
0x45, 0x5a, 0x88, 0xba, 0x16, 0xfc, 0x24, 0xea, 0x4a, 0xbc, 0x74, 0xfb, 0xbd, 0xe3, 0xd6, 0xfc,
|
||||
0xcb, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, 0xc4, 0xfc, 0xdc, 0x27, 0x48, 0x08, 0x00, 0x00,
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
// Message represents a Pub/Sub message.
|
||||
type Message struct {
|
||||
// ID identifies this message.
|
||||
// This ID is assigned by the server and is populated for Messages obtained from a subscription.
|
||||
// This field is read-only.
|
||||
ID string
|
||||
|
||||
// Data is the actual data in the message.
|
||||
Data []byte
|
||||
|
||||
// Attributes represents the key-value pairs the current message
|
||||
// is labelled with.
|
||||
Attributes map[string]string
|
||||
|
||||
// ackID is the identifier to acknowledge this message.
|
||||
ackID string
|
||||
|
||||
// The time at which the message was published.
|
||||
// This is populated by the server for Messages obtained from a subscription.
|
||||
// This field is read-only.
|
||||
PublishTime time.Time
|
||||
|
||||
// receiveTime is the time the message was received by the client.
|
||||
receiveTime time.Time
|
||||
|
||||
// size is the approximate size of the message's data and attributes.
|
||||
size int
|
||||
|
||||
calledDone bool
|
||||
|
||||
// The done method of the iterator that created this Message.
|
||||
doneFunc func(string, bool, time.Time)
|
||||
}
|
||||
|
||||
func toMessage(resp *pb.ReceivedMessage) (*Message, error) {
|
||||
if resp.Message == nil {
|
||||
return &Message{ackID: resp.AckId}, nil
|
||||
}
|
||||
|
||||
pubTime, err := ptypes.Timestamp(resp.Message.PublishTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Message{
|
||||
ackID: resp.AckId,
|
||||
Data: resp.Message.Data,
|
||||
Attributes: resp.Message.Attributes,
|
||||
ID: resp.Message.MessageId,
|
||||
PublishTime: pubTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Ack indicates successful processing of a Message passed to the Subscriber.Receive callback.
|
||||
// It should not be called on any other Message value.
|
||||
// If message acknowledgement fails, the Message will be redelivered.
|
||||
// Client code must call Ack or Nack when finished for each received Message.
|
||||
// Calls to Ack or Nack have no effect after the first call.
|
||||
func (m *Message) Ack() {
|
||||
m.done(true)
|
||||
}
|
||||
|
||||
// Nack indicates that the client will not or cannot process a Message passed to the Subscriber.Receive callback.
|
||||
// It should not be called on any other Message value.
|
||||
// Nack will result in the Message being redelivered more quickly than if it were allowed to expire.
|
||||
// Client code must call Ack or Nack when finished for each received Message.
|
||||
// Calls to Ack or Nack have no effect after the first call.
|
||||
func (m *Message) Nack() {
|
||||
m.done(false)
|
||||
}
|
||||
|
||||
func (m *Message) done(ack bool) {
|
||||
if m.calledDone {
|
||||
return
|
||||
}
|
||||
m.calledDone = true
|
||||
m.doneFunc(m.ackID, ack, m.receiveTime)
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// +build !go1.8
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
// OpenCensus only supports go 1.8 and higher.
|
||||
|
||||
func openCensusOptions() []option.ClientOption { return nil }
|
||||
|
||||
func withSubscriptionKey(ctx context.Context, _ string) context.Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
type dummy struct{}
|
||||
|
||||
var (
|
||||
// Not supported below Go 1.8.
|
||||
PullCount dummy
|
||||
// Not supported below Go 1.8.
|
||||
AckCount dummy
|
||||
// Not supported below Go 1.8.
|
||||
NackCount dummy
|
||||
// Not supported below Go 1.8.
|
||||
ModAckCount dummy
|
||||
// Not supported below Go 1.8.
|
||||
StreamOpenCount dummy
|
||||
// Not supported below Go 1.8.
|
||||
StreamRetryCount dummy
|
||||
// Not supported below Go 1.8.
|
||||
StreamRequestCount dummy
|
||||
// Not supported below Go 1.8.
|
||||
StreamResponseCount dummy
|
||||
)
|
||||
|
||||
func recordStat(context.Context, dummy, int64) {
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pstest_test
|
||||
|
||||
import (
|
||||
"cloud.google.com/go/pubsub"
|
||||
"cloud.google.com/go/pubsub/pstest"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func ExampleNewServer() {
|
||||
ctx := context.Background()
|
||||
// Start a fake server running locally.
|
||||
srv := pstest.NewServer()
|
||||
// Connect to the server without using TLS.
|
||||
conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
// Use the connection when creating a pubsub client.
|
||||
client, err := pubsub.NewClient(ctx, "project", option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
// TODO: Handle error.
|
||||
}
|
||||
defer client.Close()
|
||||
_ = client // TODO: Use the client.
|
||||
}
|
||||
+771
@@ -0,0 +1,771 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Package pstest provides a fake Cloud PubSub service for testing. It implements a
|
||||
// simplified form of the service, suitable for unit tests. It may behave
|
||||
// differently from the actual service in ways in which the service is
|
||||
// non-deterministic or unspecified: timing, delivery order, etc.
|
||||
//
|
||||
// This package is EXPERIMENTAL and is subject to change without notice.
|
||||
//
|
||||
// See the example for usage.
|
||||
package pstest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
durpb "github.com/golang/protobuf/ptypes/duration"
|
||||
emptypb "github.com/golang/protobuf/ptypes/empty"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// For testing. Note that even though changes to the now variable are atomic, a call
|
||||
// to the stored function can race with a change to that function. This could be a
|
||||
// problem if tests are run in parallel, or even if concurrent parts of the same test
|
||||
// change the value of the variable.
|
||||
var now atomic.Value
|
||||
|
||||
func init() {
|
||||
now.Store(time.Now)
|
||||
}
|
||||
|
||||
func timeNow() time.Time {
|
||||
return now.Load().(func() time.Time)()
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Addr string // The address that the server is listening on.
|
||||
gServer gServer
|
||||
}
|
||||
|
||||
type gServer struct {
|
||||
pb.PublisherServer
|
||||
pb.SubscriberServer
|
||||
|
||||
mu sync.Mutex
|
||||
topics map[string]*topic
|
||||
subs map[string]*subscription
|
||||
msgs []*Message // all messages ever published
|
||||
msgsByID map[string]*Message
|
||||
wg sync.WaitGroup
|
||||
nextID int
|
||||
streamTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewServer creates a new fake server running in the current process.
|
||||
func NewServer() *Server {
|
||||
srv, err := testutil.NewServer()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("pstest.NewServer: %v", err))
|
||||
}
|
||||
s := &Server{
|
||||
Addr: srv.Addr,
|
||||
gServer: gServer{
|
||||
topics: map[string]*topic{},
|
||||
subs: map[string]*subscription{},
|
||||
msgsByID: map[string]*Message{},
|
||||
},
|
||||
}
|
||||
pb.RegisterPublisherServer(srv.Gsrv, &s.gServer)
|
||||
pb.RegisterSubscriberServer(srv.Gsrv, &s.gServer)
|
||||
srv.Start()
|
||||
return s
|
||||
}
|
||||
|
||||
// Publish behaves as if the Publish RPC was called with a message with the given
|
||||
// data and attrs. It returns the ID of the message.
|
||||
// The topic will be created if it doesn't exist.
|
||||
//
|
||||
// Publish panics if there is an error, which is appropriate for testing.
|
||||
func (s *Server) Publish(topic string, data []byte, attrs map[string]string) string {
|
||||
const topicPattern = "projects/*/topics/*"
|
||||
ok, err := path.Match(topicPattern, topic)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("topic name must be of the form %q", topicPattern))
|
||||
}
|
||||
_, _ = s.gServer.CreateTopic(nil, &pb.Topic{Name: topic})
|
||||
req := &pb.PublishRequest{
|
||||
Topic: topic,
|
||||
Messages: []*pb.PubsubMessage{{Data: data, Attributes: attrs}},
|
||||
}
|
||||
res, err := s.gServer.Publish(nil, req)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("pstest.Server.Publish: %v", err))
|
||||
}
|
||||
return res.MessageIds[0]
|
||||
}
|
||||
|
||||
// SetStreamTimeout sets the amount of time a stream will be active before it shuts
|
||||
// itself down. This mimics the real service's behavior of closing streams after 30
|
||||
// minutes. If SetStreamTimeout is never called or is passed zero, streams never shut
|
||||
// down.
|
||||
func (s *Server) SetStreamTimeout(d time.Duration) {
|
||||
s.gServer.mu.Lock()
|
||||
defer s.gServer.mu.Unlock()
|
||||
s.gServer.streamTimeout = d
|
||||
}
|
||||
|
||||
// A Message is a message that was published to the server.
|
||||
type Message struct {
|
||||
ID string
|
||||
Data []byte
|
||||
Attributes map[string]string
|
||||
PublishTime time.Time
|
||||
Deliveries int // number of times delivery of the message was attempted
|
||||
Acks int // number of acks received from clients
|
||||
|
||||
// protected by server mutex
|
||||
deliveries int
|
||||
acks int
|
||||
}
|
||||
|
||||
// Messages returns information about all messages ever published.
|
||||
func (s *Server) Messages() []*Message {
|
||||
s.gServer.mu.Lock()
|
||||
defer s.gServer.mu.Unlock()
|
||||
|
||||
var msgs []*Message
|
||||
for _, m := range s.gServer.msgs {
|
||||
m.Deliveries = m.deliveries
|
||||
m.Acks = m.acks
|
||||
msgs = append(msgs, m)
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// Message returns the message with the given ID, or nil if no message
|
||||
// with that ID was published.
|
||||
func (s *Server) Message(id string) *Message {
|
||||
s.gServer.mu.Lock()
|
||||
defer s.gServer.mu.Unlock()
|
||||
|
||||
m := s.gServer.msgsByID[id]
|
||||
if m != nil {
|
||||
m.Deliveries = m.deliveries
|
||||
m.Acks = m.acks
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Wait blocks until all server activity has completed.
|
||||
func (s *Server) Wait() {
|
||||
s.gServer.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *gServer) CreateTopic(_ context.Context, t *pb.Topic) (*pb.Topic, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.topics[t.Name] != nil {
|
||||
return nil, status.Errorf(codes.AlreadyExists, "topic %q", t.Name)
|
||||
}
|
||||
top := newTopic(t)
|
||||
s.topics[t.Name] = top
|
||||
return top.proto, nil
|
||||
}
|
||||
|
||||
func (s *gServer) GetTopic(_ context.Context, req *pb.GetTopicRequest) (*pb.Topic, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if t := s.topics[req.Topic]; t != nil {
|
||||
return t.proto, nil
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "topic %q", req.Topic)
|
||||
}
|
||||
|
||||
func (s *gServer) UpdateTopic(_ context.Context, req *pb.UpdateTopicRequest) (*pb.Topic, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "unimplemented")
|
||||
}
|
||||
|
||||
func (s *gServer) ListTopics(_ context.Context, req *pb.ListTopicsRequest) (*pb.ListTopicsResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var names []string
|
||||
for n := range s.topics {
|
||||
if strings.HasPrefix(n, req.Project) {
|
||||
names = append(names, n)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
from, to, nextToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(names))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &pb.ListTopicsResponse{NextPageToken: nextToken}
|
||||
for i := from; i < to; i++ {
|
||||
res.Topics = append(res.Topics, s.topics[names[i]].proto)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *gServer) ListTopicSubscriptions(_ context.Context, req *pb.ListTopicSubscriptionsRequest) (*pb.ListTopicSubscriptionsResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var names []string
|
||||
for name, sub := range s.subs {
|
||||
if sub.topic.proto.Name == req.Topic {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
from, to, nextToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(names))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pb.ListTopicSubscriptionsResponse{
|
||||
Subscriptions: names[from:to],
|
||||
NextPageToken: nextToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *gServer) DeleteTopic(_ context.Context, req *pb.DeleteTopicRequest) (*emptypb.Empty, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
t := s.topics[req.Topic]
|
||||
if t == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "topic %q", req.Topic)
|
||||
}
|
||||
t.stop()
|
||||
delete(s.topics, req.Topic)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *gServer) CreateSubscription(_ context.Context, ps *pb.Subscription) (*pb.Subscription, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if ps.Name == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "missing name")
|
||||
}
|
||||
if s.subs[ps.Name] != nil {
|
||||
return nil, status.Errorf(codes.AlreadyExists, "subscription %q", ps.Name)
|
||||
}
|
||||
if ps.Topic == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "missing topic")
|
||||
}
|
||||
top := s.topics[ps.Topic]
|
||||
if top == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "topic %q", ps.Topic)
|
||||
}
|
||||
if err := checkAckDeadline(ps.AckDeadlineSeconds); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ps.MessageRetentionDuration == nil {
|
||||
ps.MessageRetentionDuration = defaultMessageRetentionDuration
|
||||
}
|
||||
if err := checkMRD(ps.MessageRetentionDuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ps.PushConfig == nil {
|
||||
ps.PushConfig = &pb.PushConfig{}
|
||||
}
|
||||
|
||||
sub := newSubscription(top, &s.mu, ps)
|
||||
top.subs[ps.Name] = sub
|
||||
s.subs[ps.Name] = sub
|
||||
sub.start(&s.wg)
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
// Can be set for testing.
|
||||
var minAckDeadlineSecs int32 = 10
|
||||
|
||||
func checkAckDeadline(ads int32) error {
|
||||
if ads < minAckDeadlineSecs || ads > 600 {
|
||||
// PubSub service returns Unknown.
|
||||
return status.Errorf(codes.Unknown, "bad ack_deadline_seconds: %d", ads)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
minMessageRetentionDuration = 10 * time.Minute
|
||||
maxMessageRetentionDuration = 168 * time.Hour
|
||||
)
|
||||
|
||||
var defaultMessageRetentionDuration = ptypes.DurationProto(maxMessageRetentionDuration)
|
||||
|
||||
func checkMRD(pmrd *durpb.Duration) error {
|
||||
mrd, err := ptypes.Duration(pmrd)
|
||||
if err != nil || mrd < minMessageRetentionDuration || mrd > maxMessageRetentionDuration {
|
||||
return status.Errorf(codes.InvalidArgument, "bad message_retention_duration %+v", pmrd)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *gServer) GetSubscription(_ context.Context, req *pb.GetSubscriptionRequest) (*pb.Subscription, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if sub := s.subs[req.Subscription]; sub != nil {
|
||||
return sub.proto, nil
|
||||
}
|
||||
return nil, status.Errorf(codes.NotFound, "subscription %q", req.Subscription)
|
||||
}
|
||||
|
||||
func (s *gServer) UpdateSubscription(_ context.Context, req *pb.UpdateSubscriptionRequest) (*pb.Subscription, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
sub := s.subs[req.Subscription.Name]
|
||||
if sub == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "subscription %q", req.Subscription.Name)
|
||||
}
|
||||
|
||||
for _, path := range req.UpdateMask.Paths {
|
||||
switch path {
|
||||
case "push_config":
|
||||
sub.proto.PushConfig = req.Subscription.PushConfig
|
||||
|
||||
case "ack_deadline_seconds":
|
||||
a := req.Subscription.AckDeadlineSeconds
|
||||
if err := checkAckDeadline(a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.proto.AckDeadlineSeconds = a
|
||||
|
||||
case "retain_acked_messages":
|
||||
sub.proto.RetainAckedMessages = req.Subscription.RetainAckedMessages
|
||||
|
||||
case "message_retention_duration":
|
||||
if err := checkMRD(req.Subscription.MessageRetentionDuration); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sub.proto.MessageRetentionDuration = req.Subscription.MessageRetentionDuration
|
||||
|
||||
// TODO(jba): labels
|
||||
default:
|
||||
return nil, status.Errorf(codes.InvalidArgument, "unknown field name %q", path)
|
||||
}
|
||||
}
|
||||
return sub.proto, nil
|
||||
}
|
||||
|
||||
func (s *gServer) ListSubscriptions(_ context.Context, req *pb.ListSubscriptionsRequest) (*pb.ListSubscriptionsResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var names []string
|
||||
for name := range s.subs {
|
||||
if strings.HasPrefix(name, req.Project) {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
from, to, nextToken, err := testutil.PageBounds(int(req.PageSize), req.PageToken, len(names))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := &pb.ListSubscriptionsResponse{NextPageToken: nextToken}
|
||||
for i := from; i < to; i++ {
|
||||
res.Subscriptions = append(res.Subscriptions, s.subs[names[i]].proto)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *gServer) DeleteSubscription(_ context.Context, req *pb.DeleteSubscriptionRequest) (*emptypb.Empty, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
sub := s.subs[req.Subscription]
|
||||
if sub == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "subscription %q", req.Subscription)
|
||||
}
|
||||
sub.stop()
|
||||
delete(s.subs, req.Subscription)
|
||||
sub.topic.deleteSub(sub)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *gServer) Publish(_ context.Context, req *pb.PublishRequest) (*pb.PublishResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if req.Topic == "" {
|
||||
return nil, status.Errorf(codes.InvalidArgument, "missing topic")
|
||||
}
|
||||
top := s.topics[req.Topic]
|
||||
if top == nil {
|
||||
return nil, status.Errorf(codes.NotFound, "topic %q", req.Topic)
|
||||
}
|
||||
var ids []string
|
||||
for _, pm := range req.Messages {
|
||||
id := fmt.Sprintf("m%d", s.nextID)
|
||||
s.nextID++
|
||||
pm.MessageId = id
|
||||
pubTime := timeNow()
|
||||
tsPubTime, err := ptypes.TimestampProto(pubTime)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, err.Error())
|
||||
}
|
||||
pm.PublishTime = tsPubTime
|
||||
m := &Message{
|
||||
ID: id,
|
||||
Data: pm.Data,
|
||||
Attributes: pm.Attributes,
|
||||
PublishTime: pubTime,
|
||||
}
|
||||
top.publish(pm, m)
|
||||
ids = append(ids, id)
|
||||
s.msgs = append(s.msgs, m)
|
||||
s.msgsByID[id] = m
|
||||
}
|
||||
return &pb.PublishResponse{MessageIds: ids}, nil
|
||||
}
|
||||
|
||||
type topic struct {
|
||||
proto *pb.Topic
|
||||
subs map[string]*subscription
|
||||
}
|
||||
|
||||
func newTopic(pt *pb.Topic) *topic {
|
||||
return &topic{
|
||||
proto: pt,
|
||||
subs: map[string]*subscription{},
|
||||
}
|
||||
}
|
||||
|
||||
func (t *topic) stop() {
|
||||
for _, sub := range t.subs {
|
||||
sub.proto.Topic = "_deleted-topic_"
|
||||
sub.stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *topic) deleteSub(sub *subscription) {
|
||||
delete(t.subs, sub.proto.Name)
|
||||
}
|
||||
|
||||
func (t *topic) publish(pm *pb.PubsubMessage, m *Message) {
|
||||
for _, s := range t.subs {
|
||||
s.msgs[pm.MessageId] = &message{
|
||||
publishTime: m.PublishTime,
|
||||
proto: &pb.ReceivedMessage{
|
||||
AckId: pm.MessageId,
|
||||
Message: pm,
|
||||
},
|
||||
deliveries: &m.deliveries,
|
||||
acks: &m.acks,
|
||||
streamIndex: -1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type subscription struct {
|
||||
topic *topic
|
||||
mu *sync.Mutex
|
||||
proto *pb.Subscription
|
||||
ackTimeout time.Duration
|
||||
msgs map[string]*message // unacked messages by message ID
|
||||
streams []*stream
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func newSubscription(t *topic, mu *sync.Mutex, ps *pb.Subscription) *subscription {
|
||||
at := time.Duration(ps.AckDeadlineSeconds) * time.Second
|
||||
if at == 0 {
|
||||
at = 10 * time.Second
|
||||
}
|
||||
return &subscription{
|
||||
topic: t,
|
||||
mu: mu,
|
||||
proto: ps,
|
||||
ackTimeout: at,
|
||||
msgs: map[string]*message{},
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscription) start(wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
case <-time.After(10 * time.Millisecond):
|
||||
s.deliver()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *subscription) stop() {
|
||||
close(s.done)
|
||||
}
|
||||
|
||||
func (s *gServer) StreamingPull(sps pb.Subscriber_StreamingPullServer) error {
|
||||
// Receive initial message configuring the pull.
|
||||
req, err := sps.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Subscription == "" {
|
||||
return status.Errorf(codes.InvalidArgument, "missing subscription")
|
||||
}
|
||||
s.mu.Lock()
|
||||
sub := s.subs[req.Subscription]
|
||||
s.mu.Unlock()
|
||||
if sub == nil {
|
||||
return status.Errorf(codes.NotFound, "subscription %s", req.Subscription)
|
||||
}
|
||||
// Create a new stream to handle the pull.
|
||||
st := sub.newStream(sps, s.streamTimeout)
|
||||
err = st.pull(&s.wg)
|
||||
sub.deleteStream(st)
|
||||
return err
|
||||
}
|
||||
|
||||
var retentionDuration = 10 * time.Minute
|
||||
|
||||
func (s *subscription) deliver() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
tNow := timeNow()
|
||||
for id, m := range s.msgs {
|
||||
// Mark a message as re-deliverable if its ack deadline has expired.
|
||||
if m.outstanding() && tNow.After(m.ackDeadline) {
|
||||
m.makeAvailable()
|
||||
}
|
||||
pubTime, err := ptypes.Timestamp(m.proto.Message.PublishTime)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Remove messages that have been undelivered for a long time.
|
||||
if !m.outstanding() && tNow.Sub(pubTime) > retentionDuration {
|
||||
delete(s.msgs, id)
|
||||
}
|
||||
}
|
||||
// Try to deliver each remaining message.
|
||||
curIndex := 0
|
||||
for _, m := range s.msgs {
|
||||
if m.outstanding() {
|
||||
continue
|
||||
}
|
||||
// If the message was never delivered before, start with the stream at
|
||||
// curIndex. If it was delivered before, start with the stream after the one
|
||||
// that owned it.
|
||||
if m.streamIndex < 0 {
|
||||
delIndex, ok := s.deliverMessage(m, curIndex, tNow)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
curIndex = delIndex + 1
|
||||
m.streamIndex = curIndex
|
||||
} else {
|
||||
delIndex, ok := s.deliverMessage(m, m.streamIndex, tNow)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
m.streamIndex = delIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// deliverMessage attempts to deliver m to the stream at index i. If it can't, it
|
||||
// tries streams i+1, i+2, ..., wrapping around. It returns the index of the stream
|
||||
// it delivered the message to, or 0, false if it didn't deliver the message because
|
||||
// there are no active streams.
|
||||
func (s *subscription) deliverMessage(m *message, i int, tNow time.Time) (int, bool) {
|
||||
for len(s.streams) > 0 {
|
||||
if i >= len(s.streams) {
|
||||
i = 0
|
||||
}
|
||||
st := s.streams[i]
|
||||
select {
|
||||
case <-st.done:
|
||||
s.streams = deleteStreamAt(s.streams, i)
|
||||
|
||||
case st.msgc <- m.proto:
|
||||
(*m.deliveries)++
|
||||
m.ackDeadline = tNow.Add(st.ackTimeout)
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (s *subscription) newStream(gs pb.Subscriber_StreamingPullServer, timeout time.Duration) *stream {
|
||||
st := &stream{
|
||||
sub: s,
|
||||
done: make(chan struct{}),
|
||||
msgc: make(chan *pb.ReceivedMessage),
|
||||
gstream: gs,
|
||||
ackTimeout: s.ackTimeout,
|
||||
timeout: timeout,
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.streams = append(s.streams, st)
|
||||
s.mu.Unlock()
|
||||
return st
|
||||
}
|
||||
|
||||
func (s *subscription) deleteStream(st *stream) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
var i int
|
||||
for i = 0; i < len(s.streams); i++ {
|
||||
if s.streams[i] == st {
|
||||
break
|
||||
}
|
||||
}
|
||||
if i < len(s.streams) {
|
||||
s.streams = deleteStreamAt(s.streams, i)
|
||||
}
|
||||
}
|
||||
func deleteStreamAt(s []*stream, i int) []*stream {
|
||||
// Preserve order for round-robin delivery.
|
||||
return append(s[:i], s[i+1:]...)
|
||||
}
|
||||
|
||||
type message struct {
|
||||
proto *pb.ReceivedMessage
|
||||
publishTime time.Time
|
||||
ackDeadline time.Time
|
||||
deliveries *int
|
||||
acks *int
|
||||
streamIndex int // index of stream that currently owns msg, for round-robin delivery
|
||||
}
|
||||
|
||||
// A message is outstanding if it is owned by some stream.
|
||||
func (m *message) outstanding() bool {
|
||||
return !m.ackDeadline.IsZero()
|
||||
}
|
||||
|
||||
func (m *message) makeAvailable() {
|
||||
m.ackDeadline = time.Time{}
|
||||
}
|
||||
|
||||
type stream struct {
|
||||
sub *subscription
|
||||
done chan struct{} // closed when the stream is finished
|
||||
msgc chan *pb.ReceivedMessage
|
||||
gstream pb.Subscriber_StreamingPullServer
|
||||
ackTimeout time.Duration
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// pull manages the StreamingPull interaction for the life of the stream.
|
||||
func (st *stream) pull(wg *sync.WaitGroup) error {
|
||||
errc := make(chan error, 2)
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errc <- st.sendLoop()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
errc <- st.recvLoop()
|
||||
}()
|
||||
var tchan <-chan time.Time
|
||||
if st.timeout > 0 {
|
||||
tchan = time.After(st.timeout)
|
||||
}
|
||||
// Wait until one of the goroutines returns an error, or we time out.
|
||||
var err error
|
||||
select {
|
||||
case err = <-errc:
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
case <-tchan:
|
||||
}
|
||||
close(st.done) // stop the other goroutine
|
||||
return err
|
||||
}
|
||||
|
||||
func (st *stream) sendLoop() error {
|
||||
for {
|
||||
select {
|
||||
case <-st.done:
|
||||
return nil
|
||||
case rm := <-st.msgc:
|
||||
res := &pb.StreamingPullResponse{ReceivedMessages: []*pb.ReceivedMessage{rm}}
|
||||
if err := st.gstream.Send(res); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (st *stream) recvLoop() error {
|
||||
for {
|
||||
req, err := st.gstream.Recv()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st.sub.handleStreamingPullRequest(st, req)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscription) handleStreamingPullRequest(st *stream, req *pb.StreamingPullRequest) {
|
||||
// Lock the entire server.
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for _, ackID := range req.AckIds {
|
||||
s.ack(ackID)
|
||||
}
|
||||
for i, id := range req.ModifyDeadlineAckIds {
|
||||
s.modifyAckDeadline(id, secsToDur(req.ModifyDeadlineSeconds[i]))
|
||||
}
|
||||
if req.StreamAckDeadlineSeconds > 0 {
|
||||
st.ackTimeout = secsToDur(req.StreamAckDeadlineSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscription) ack(id string) {
|
||||
m := s.msgs[id]
|
||||
if m != nil {
|
||||
(*m.acks)++
|
||||
delete(s.msgs, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscription) modifyAckDeadline(id string, d time.Duration) {
|
||||
m := s.msgs[id]
|
||||
if m == nil { // already acked: ignore.
|
||||
return
|
||||
}
|
||||
if d == 0 { // nack
|
||||
m.makeAvailable()
|
||||
} else { // extend the deadline by d
|
||||
m.ackDeadline = timeNow().Add(d)
|
||||
}
|
||||
}
|
||||
|
||||
func secsToDur(secs int32) time.Duration {
|
||||
return time.Duration(secs) * time.Second
|
||||
}
|
||||
+434
@@ -0,0 +1,434 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pstest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestTopics(t *testing.T) {
|
||||
pclient, _, server := newFake(t)
|
||||
ctx := context.Background()
|
||||
var topics []*pb.Topic
|
||||
for i := 1; i < 3; i++ {
|
||||
topics = append(topics, mustCreateTopic(t, pclient, &pb.Topic{
|
||||
Name: fmt.Sprintf("projects/P/topics/T%d", i),
|
||||
Labels: map[string]string{"num": fmt.Sprintf("%d", i)},
|
||||
}))
|
||||
}
|
||||
if got, want := len(server.gServer.topics), len(topics); got != want {
|
||||
t.Fatalf("got %d topics, want %d", got, want)
|
||||
}
|
||||
for _, top := range topics {
|
||||
got, err := pclient.GetTopic(ctx, &pb.GetTopicRequest{Topic: top.Name})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testutil.Equal(got, top) {
|
||||
t.Errorf("\ngot %+v\nwant %+v", got, top)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := pclient.ListTopics(ctx, &pb.ListTopicsRequest{Project: "projects/P"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := res.Topics, topics; !testutil.Equal(got, want) {
|
||||
t.Errorf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
|
||||
for _, top := range topics {
|
||||
if _, err := pclient.DeleteTopic(ctx, &pb.DeleteTopicRequest{Topic: top.Name}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if got, want := len(server.gServer.topics), 0; got != want {
|
||||
t.Fatalf("got %d topics, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptions(t *testing.T) {
|
||||
pclient, sclient, server := newFake(t)
|
||||
ctx := context.Background()
|
||||
topic := mustCreateTopic(t, pclient, &pb.Topic{Name: "projects/P/topics/T"})
|
||||
var subs []*pb.Subscription
|
||||
for i := 0; i < 3; i++ {
|
||||
subs = append(subs, mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: fmt.Sprintf("projects/P/subscriptions/S%d", i),
|
||||
Topic: topic.Name,
|
||||
AckDeadlineSeconds: int32(10 * (i + 1)),
|
||||
}))
|
||||
}
|
||||
|
||||
if got, want := len(server.gServer.subs), len(subs); got != want {
|
||||
t.Fatalf("got %d subscriptions, want %d", got, want)
|
||||
}
|
||||
for _, s := range subs {
|
||||
got, err := sclient.GetSubscription(ctx, &pb.GetSubscriptionRequest{Subscription: s.Name})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !testutil.Equal(got, s) {
|
||||
t.Errorf("\ngot %+v\nwant %+v", got, s)
|
||||
}
|
||||
}
|
||||
|
||||
res, err := sclient.ListSubscriptions(ctx, &pb.ListSubscriptionsRequest{Project: "projects/P"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := res.Subscriptions, subs; !testutil.Equal(got, want) {
|
||||
t.Errorf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
|
||||
res2, err := pclient.ListTopicSubscriptions(ctx, &pb.ListTopicSubscriptionsRequest{Topic: topic.Name})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := len(res2.Subscriptions), len(subs); got != want {
|
||||
t.Fatalf("got %d subs, want %d", got, want)
|
||||
}
|
||||
for i, got := range res2.Subscriptions {
|
||||
want := subs[i].Name
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Errorf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
for _, s := range subs {
|
||||
if _, err := sclient.DeleteSubscription(ctx, &pb.DeleteSubscriptionRequest{Subscription: s.Name}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if got, want := len(server.gServer.subs), 0; got != want {
|
||||
t.Fatalf("got %d subscriptions, want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
s := NewServer()
|
||||
var ids []string
|
||||
for i := 0; i < 3; i++ {
|
||||
ids = append(ids, s.Publish("projects/p/topics/t", []byte("hello"), nil))
|
||||
}
|
||||
s.Wait()
|
||||
ms := s.Messages()
|
||||
if got, want := len(ms), len(ids); got != want {
|
||||
t.Errorf("got %d messages, want %d", got, want)
|
||||
}
|
||||
for i, id := range ids {
|
||||
if got, want := ms[i].ID, id; got != want {
|
||||
t.Errorf("got %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
m := s.Message(ids[1])
|
||||
if m == nil {
|
||||
t.Error("got nil, want a message")
|
||||
}
|
||||
}
|
||||
|
||||
// Note: this sets the fake's "now" time, so it is senstive to concurrent changes to "now".
|
||||
func publish(t *testing.T, pclient pb.PublisherClient, topic *pb.Topic, messages []*pb.PubsubMessage) map[string]*pb.PubsubMessage {
|
||||
pubTime := time.Now()
|
||||
now.Store(func() time.Time { return pubTime })
|
||||
defer func() { now.Store(time.Now) }()
|
||||
|
||||
res, err := pclient.Publish(context.Background(), &pb.PublishRequest{
|
||||
Topic: topic.Name,
|
||||
Messages: messages,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tsPubTime, err := ptypes.TimestampProto(pubTime)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := map[string]*pb.PubsubMessage{}
|
||||
for i, id := range res.MessageIds {
|
||||
want[id] = &pb.PubsubMessage{
|
||||
Data: messages[i].Data,
|
||||
Attributes: messages[i].Attributes,
|
||||
MessageId: id,
|
||||
PublishTime: tsPubTime,
|
||||
}
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
func TestStreamingPull(t *testing.T) {
|
||||
// A simple test of streaming pull.
|
||||
pclient, sclient, _ := newFake(t)
|
||||
top := mustCreateTopic(t, pclient, &pb.Topic{Name: "projects/P/topics/T"})
|
||||
sub := mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: "projects/P/subscriptions/S",
|
||||
Topic: top.Name,
|
||||
AckDeadlineSeconds: 10,
|
||||
})
|
||||
|
||||
want := publish(t, pclient, top, []*pb.PubsubMessage{
|
||||
{Data: []byte("d1")},
|
||||
{Data: []byte("d2")},
|
||||
{Data: []byte("d3")},
|
||||
})
|
||||
got := pullN(t, len(want), sclient, sub)
|
||||
if diff := testutil.Diff(got, want); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAck(t *testing.T) {
|
||||
// Ack each message as it arrives. Make sure we don't see dups.
|
||||
minAckDeadlineSecs = 1
|
||||
pclient, sclient, _ := newFake(t)
|
||||
top := mustCreateTopic(t, pclient, &pb.Topic{Name: "projects/P/topics/T"})
|
||||
sub := mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: "projects/P/subscriptions/S",
|
||||
Topic: top.Name,
|
||||
AckDeadlineSeconds: 1,
|
||||
})
|
||||
|
||||
_ = publish(t, pclient, top, []*pb.PubsubMessage{
|
||||
{Data: []byte("d1")},
|
||||
{Data: []byte("d2")},
|
||||
{Data: []byte("d3")},
|
||||
})
|
||||
|
||||
got := map[string]bool{}
|
||||
spc := mustStartPull(t, sclient, sub)
|
||||
time.AfterFunc(time.Duration(3*minAckDeadlineSecs)*time.Second, func() {
|
||||
if err := spc.CloseSend(); err != nil {
|
||||
t.Errorf("CloseSend: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
for {
|
||||
res, err := spc.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := &pb.StreamingPullRequest{}
|
||||
for _, m := range res.ReceivedMessages {
|
||||
if got[m.Message.MessageId] {
|
||||
t.Fatal("duplicate message")
|
||||
}
|
||||
got[m.Message.MessageId] = true
|
||||
req.AckIds = append(req.AckIds, m.AckId)
|
||||
}
|
||||
if err := spc.Send(req); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAckDeadline(t *testing.T) {
|
||||
// Messages should be resent after they expire.
|
||||
pclient, sclient, _ := newFake(t)
|
||||
minAckDeadlineSecs = 2
|
||||
top := mustCreateTopic(t, pclient, &pb.Topic{Name: "projects/P/topics/T"})
|
||||
sub := mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: "projects/P/subscriptions/S",
|
||||
Topic: top.Name,
|
||||
AckDeadlineSeconds: minAckDeadlineSecs,
|
||||
})
|
||||
|
||||
_ = publish(t, pclient, top, []*pb.PubsubMessage{
|
||||
{Data: []byte("d1")},
|
||||
{Data: []byte("d2")},
|
||||
{Data: []byte("d3")},
|
||||
})
|
||||
|
||||
got := map[string]int{}
|
||||
spc := mustStartPull(t, sclient, sub)
|
||||
// In 5 seconds the ack deadline will expire twice, so we should see each message
|
||||
// exactly three times.
|
||||
time.AfterFunc(5*time.Second, func() {
|
||||
if err := spc.CloseSend(); err != nil {
|
||||
t.Errorf("CloseSend: %v", err)
|
||||
}
|
||||
})
|
||||
for {
|
||||
res, err := spc.Recv()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range res.ReceivedMessages {
|
||||
got[m.Message.MessageId]++
|
||||
}
|
||||
}
|
||||
for id, n := range got {
|
||||
if n != 3 {
|
||||
t.Errorf("message %s: saw %d times, want 3", id, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiSubs(t *testing.T) {
|
||||
// Each subscription gets every message.
|
||||
pclient, sclient, _ := newFake(t)
|
||||
top := mustCreateTopic(t, pclient, &pb.Topic{Name: "projects/P/topics/T"})
|
||||
sub1 := mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: "projects/P/subscriptions/S1",
|
||||
Topic: top.Name,
|
||||
AckDeadlineSeconds: 10,
|
||||
})
|
||||
sub2 := mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: "projects/P/subscriptions/S2",
|
||||
Topic: top.Name,
|
||||
AckDeadlineSeconds: 10,
|
||||
})
|
||||
|
||||
want := publish(t, pclient, top, []*pb.PubsubMessage{
|
||||
{Data: []byte("d1")},
|
||||
{Data: []byte("d2")},
|
||||
{Data: []byte("d3")},
|
||||
})
|
||||
got1 := pullN(t, len(want), sclient, sub1)
|
||||
got2 := pullN(t, len(want), sclient, sub2)
|
||||
if diff := testutil.Diff(got1, want); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
if diff := testutil.Diff(got2, want); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultiStreams(t *testing.T) {
|
||||
// Messages are handed out to the streams of a subscription in round-robin order.
|
||||
pclient, sclient, _ := newFake(t)
|
||||
top := mustCreateTopic(t, pclient, &pb.Topic{Name: "projects/P/topics/T"})
|
||||
sub := mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: "projects/P/subscriptions/S",
|
||||
Topic: top.Name,
|
||||
AckDeadlineSeconds: 10,
|
||||
})
|
||||
want := publish(t, pclient, top, []*pb.PubsubMessage{
|
||||
{Data: []byte("d1")},
|
||||
{Data: []byte("d2")},
|
||||
{Data: []byte("d3")},
|
||||
{Data: []byte("d4")},
|
||||
})
|
||||
streams := []pb.Subscriber_StreamingPullClient{
|
||||
mustStartPull(t, sclient, sub),
|
||||
mustStartPull(t, sclient, sub),
|
||||
}
|
||||
got := map[string]*pb.PubsubMessage{}
|
||||
for i := 0; i < 2; i++ {
|
||||
for _, st := range streams {
|
||||
res, err := st.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
m := res.ReceivedMessages[0]
|
||||
got[m.Message.MessageId] = m.Message
|
||||
}
|
||||
}
|
||||
if diff := testutil.Diff(got, want); diff != "" {
|
||||
t.Error(diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingPullTimeout(t *testing.T) {
|
||||
pclient, sclient, srv := newFake(t)
|
||||
timeout := 200 * time.Millisecond
|
||||
srv.SetStreamTimeout(timeout)
|
||||
top := mustCreateTopic(t, pclient, &pb.Topic{Name: "projects/P/topics/T"})
|
||||
sub := mustCreateSubscription(t, sclient, &pb.Subscription{
|
||||
Name: "projects/P/subscriptions/S",
|
||||
Topic: top.Name,
|
||||
AckDeadlineSeconds: 10,
|
||||
})
|
||||
stream := mustStartPull(t, sclient, sub)
|
||||
time.Sleep(2 * timeout)
|
||||
_, err := stream.Recv()
|
||||
if err != io.EOF {
|
||||
t.Errorf("got %v, want io.EOF", err)
|
||||
}
|
||||
}
|
||||
|
||||
func mustStartPull(t *testing.T, sc pb.SubscriberClient, sub *pb.Subscription) pb.Subscriber_StreamingPullClient {
|
||||
spc, err := sc.StreamingPull(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := spc.Send(&pb.StreamingPullRequest{Subscription: sub.Name}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return spc
|
||||
}
|
||||
|
||||
func pullN(t *testing.T, n int, sc pb.SubscriberClient, sub *pb.Subscription) map[string]*pb.PubsubMessage {
|
||||
spc := mustStartPull(t, sc, sub)
|
||||
got := map[string]*pb.PubsubMessage{}
|
||||
for i := 0; i < n; i++ {
|
||||
res, err := spc.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, m := range res.ReceivedMessages {
|
||||
got[m.Message.MessageId] = m.Message
|
||||
}
|
||||
}
|
||||
if err := spc.CloseSend(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
res, err := spc.Recv()
|
||||
if err != io.EOF {
|
||||
t.Fatalf("Recv returned <%v> instead of EOF; res = %v", err, res)
|
||||
}
|
||||
return got
|
||||
}
|
||||
|
||||
func mustCreateTopic(t *testing.T, pc pb.PublisherClient, topic *pb.Topic) *pb.Topic {
|
||||
top, err := pc.CreateTopic(context.Background(), topic)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return top
|
||||
}
|
||||
|
||||
func mustCreateSubscription(t *testing.T, sc pb.SubscriberClient, sub *pb.Subscription) *pb.Subscription {
|
||||
sub, err := sc.CreateSubscription(context.Background(), sub)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func newFake(t *testing.T) (pb.PublisherClient, pb.SubscriberClient, *Server) {
|
||||
srv := NewServer()
|
||||
conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return pb.NewPublisherClient(conn), pb.NewSubscriberClient(conn), srv
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"cloud.google.com/go/pubsub"
|
||||
"cloud.google.com/go/pubsub/pstest"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func TestPSTest(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
srv := pstest.NewServer()
|
||||
|
||||
conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
client, err := pubsub.NewClient(ctx, "some-project", option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
topic, err := client.CreateTopic(ctx, "test-topic")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sub, err := client.CreateSubscription(ctx, "sub-name", pubsub.SubscriptionConfig{Topic: topic})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for i := 0; i < 10; i++ {
|
||||
srv.Publish("projects/some-project/topics/test-topic", []byte(strconv.Itoa(i)), nil)
|
||||
}
|
||||
}()
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
var mu sync.Mutex
|
||||
count := 0
|
||||
err = sub.Receive(ctx, func(ctx context.Context, m *pubsub.Message) {
|
||||
mu.Lock()
|
||||
count++
|
||||
if count >= 10 {
|
||||
cancel()
|
||||
}
|
||||
mu.Unlock()
|
||||
m.Ack()
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
// Copyright 2014 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub // import "cloud.google.com/go/pubsub"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/version"
|
||||
vkit "cloud.google.com/go/pubsub/apiv1"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/keepalive"
|
||||
)
|
||||
|
||||
const (
|
||||
// ScopePubSub grants permissions to view and manage Pub/Sub
|
||||
// topics and subscriptions.
|
||||
ScopePubSub = "https://www.googleapis.com/auth/pubsub"
|
||||
|
||||
// ScopeCloudPlatform grants permissions to view and manage your data
|
||||
// across Google Cloud Platform services.
|
||||
ScopeCloudPlatform = "https://www.googleapis.com/auth/cloud-platform"
|
||||
)
|
||||
|
||||
const (
|
||||
prodAddr = "https://pubsub.googleapis.com/"
|
||||
minAckDeadline = 10 * time.Second
|
||||
maxAckDeadline = 10 * time.Minute
|
||||
)
|
||||
|
||||
// Client is a Google Pub/Sub client scoped to a single project.
|
||||
//
|
||||
// Clients should be reused rather than being created as needed.
|
||||
// A Client may be shared by multiple goroutines.
|
||||
type Client struct {
|
||||
projectID string
|
||||
pubc *vkit.PublisherClient
|
||||
subc *vkit.SubscriberClient
|
||||
}
|
||||
|
||||
// NewClient creates a new PubSub client.
|
||||
func NewClient(ctx context.Context, projectID string, opts ...option.ClientOption) (c *Client, err error) {
|
||||
var o []option.ClientOption
|
||||
// Environment variables for gcloud emulator:
|
||||
// https://cloud.google.com/sdk/gcloud/reference/beta/emulators/pubsub/
|
||||
if addr := os.Getenv("PUBSUB_EMULATOR_HOST"); addr != "" {
|
||||
conn, err := grpc.Dial(addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("grpc.Dial: %v", err)
|
||||
}
|
||||
o = []option.ClientOption{option.WithGRPCConn(conn)}
|
||||
} else {
|
||||
o = []option.ClientOption{
|
||||
// Create multiple connections to increase throughput.
|
||||
option.WithGRPCConnectionPool(runtime.GOMAXPROCS(0)),
|
||||
option.WithGRPCDialOption(grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||
Time: 5 * time.Minute,
|
||||
})),
|
||||
}
|
||||
o = append(o, openCensusOptions()...)
|
||||
}
|
||||
o = append(o, opts...)
|
||||
pubc, err := vkit.NewPublisherClient(ctx, o...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pubsub: %v", err)
|
||||
}
|
||||
subc, err := vkit.NewSubscriberClient(ctx, option.WithGRPCConn(pubc.Connection()))
|
||||
if err != nil {
|
||||
// Should never happen, since we are passing in the connection.
|
||||
// If it does, we cannot close, because the user may have passed in their
|
||||
// own connection originally.
|
||||
return nil, fmt.Errorf("pubsub: %v", err)
|
||||
}
|
||||
pubc.SetGoogleClientInfo("gccl", version.Repo)
|
||||
subc.SetGoogleClientInfo("gccl", version.Repo)
|
||||
return &Client{
|
||||
projectID: projectID,
|
||||
pubc: pubc,
|
||||
subc: subc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close releases any resources held by the client,
|
||||
// such as memory and goroutines.
|
||||
//
|
||||
// If the client is available for the lifetime of the program, then Close need not be
|
||||
// called at exit.
|
||||
func (c *Client) Close() error {
|
||||
// Return the first error, because the first call closes the connection.
|
||||
err := c.pubc.Close()
|
||||
_ = c.subc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) fullyQualifiedProjectName() string {
|
||||
return fmt.Sprintf("projects/%s", c.projectID)
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
vkit "cloud.google.com/go/pubsub/apiv1"
|
||||
gax "github.com/googleapis/gax-go"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// A pullStream supports the methods of a StreamingPullClient, but re-opens
|
||||
// the stream on a retryable error.
|
||||
type pullStream struct {
|
||||
ctx context.Context
|
||||
open func() (pb.Subscriber_StreamingPullClient, error)
|
||||
|
||||
mu sync.Mutex
|
||||
spc *pb.Subscriber_StreamingPullClient
|
||||
err error // permanent error
|
||||
}
|
||||
|
||||
func newPullStream(ctx context.Context, subc *vkit.SubscriberClient, subName string, ackDeadlineSecs int32) *pullStream {
|
||||
ctx = withSubscriptionKey(ctx, subName)
|
||||
return &pullStream{
|
||||
ctx: ctx,
|
||||
open: func() (pb.Subscriber_StreamingPullClient, error) {
|
||||
spc, err := subc.StreamingPull(ctx, gax.WithGRPCOptions(grpc.MaxCallRecvMsgSize(maxSendRecvBytes)))
|
||||
if err == nil {
|
||||
recordStat(ctx, StreamRequestCount, 1)
|
||||
err = spc.Send(&pb.StreamingPullRequest{
|
||||
Subscription: subName,
|
||||
StreamAckDeadlineSeconds: ackDeadlineSecs,
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return spc, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// get returns either a valid *StreamingPullClient (SPC), or a permanent error.
|
||||
// If the argument is nil, this is the first call for an RPC, and the current
|
||||
// SPC will be returned (or a new one will be opened). Otherwise, this call is a
|
||||
// request to re-open the stream because of a retryable error, and the argument
|
||||
// is a pointer to the SPC that returned the error.
|
||||
func (s *pullStream) get(spc *pb.Subscriber_StreamingPullClient) (*pb.Subscriber_StreamingPullClient, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
// A stored error is permanent.
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
// If the context is done, so are we.
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
s.err = s.ctx.Err()
|
||||
return nil, s.err
|
||||
default:
|
||||
}
|
||||
// TODO(jba): We can use the following instead of the above after we drop support for 1.8:
|
||||
// s.err = s.ctx.Err()
|
||||
// if s.err != nil {
|
||||
// return nil, s.err
|
||||
// }
|
||||
|
||||
// If the current and argument SPCs differ, return the current one. This subsumes two cases:
|
||||
// 1. We have an SPC and the caller is getting the stream for the first time.
|
||||
// 2. The caller wants to retry, but they have an older SPC; we've already retried.
|
||||
if spc != s.spc {
|
||||
return s.spc, nil
|
||||
}
|
||||
// Either this is the very first call on this stream (s.spc == nil), or we have a valid
|
||||
// retry request. Either way, open a new stream.
|
||||
// The lock is held here for a long time, but it doesn't matter because no callers could get
|
||||
// anything done anyway.
|
||||
s.spc = new(pb.Subscriber_StreamingPullClient)
|
||||
recordStat(s.ctx, StreamOpenCount, 1)
|
||||
*s.spc, s.err = s.open() // Setting s.err means any error from open is permanent. Reconsider.
|
||||
return s.spc, s.err
|
||||
}
|
||||
|
||||
func (s *pullStream) call(f func(pb.Subscriber_StreamingPullClient) error) error {
|
||||
var (
|
||||
spc *pb.Subscriber_StreamingPullClient
|
||||
err error
|
||||
bo gax.Backoff
|
||||
)
|
||||
for {
|
||||
spc, err = s.get(spc)
|
||||
if err != nil {
|
||||
// Preserve the existing behavior of not retrying on open. Is that a bug?
|
||||
// (If we do decide to retry, don't retry after we're closed.)
|
||||
return err
|
||||
}
|
||||
err = f(*spc)
|
||||
if err != nil {
|
||||
if isRetryable(err) {
|
||||
recordStat(s.ctx, StreamRetryCount, 1)
|
||||
gax.Sleep(s.ctx, bo.Pause())
|
||||
continue
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.err = err
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *pullStream) Send(req *pb.StreamingPullRequest) error {
|
||||
return s.call(func(spc pb.Subscriber_StreamingPullClient) error {
|
||||
recordStat(s.ctx, AckCount, int64(len(req.AckIds)))
|
||||
zeroes := 0
|
||||
for _, mds := range req.ModifyDeadlineSeconds {
|
||||
if mds == 0 {
|
||||
zeroes++
|
||||
}
|
||||
}
|
||||
recordStat(s.ctx, NackCount, int64(zeroes))
|
||||
recordStat(s.ctx, ModAckCount, int64(len(req.ModifyDeadlineSeconds)-zeroes))
|
||||
recordStat(s.ctx, StreamRequestCount, 1)
|
||||
return spc.Send(req)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *pullStream) Recv() (*pb.StreamingPullResponse, error) {
|
||||
var res *pb.StreamingPullResponse
|
||||
err := s.call(func(spc pb.Subscriber_StreamingPullClient) error {
|
||||
var err error
|
||||
recordStat(s.ctx, StreamResponseCount, 1)
|
||||
res, err = spc.Recv()
|
||||
if err == nil {
|
||||
recordStat(s.ctx, PullCount, int64(len(res.ReceivedMessages)))
|
||||
}
|
||||
return err
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (s *pullStream) CloseSend() error {
|
||||
err := s.call(func(spc pb.Subscriber_StreamingPullClient) error {
|
||||
return spc.CloseSend()
|
||||
})
|
||||
s.mu.Lock()
|
||||
s.err = io.EOF // should not be retried
|
||||
s.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// maxPayload is the maximum number of bytes to devote to actual ids in
|
||||
// acknowledgement or modifyAckDeadline requests. A serialized
|
||||
// AcknowledgeRequest proto has a small constant overhead, plus the size of the
|
||||
// subscription name, plus 3 bytes per ID (a tag byte and two size bytes). A
|
||||
// ModifyAckDeadlineRequest has an additional few bytes for the deadline. We
|
||||
// don't know the subscription name here, so we just assume the size exclusive
|
||||
// of ids is 100 bytes.
|
||||
//
|
||||
// With gRPC there is no way for the client to know the server's max message size (it is
|
||||
// configurable on the server). We know from experience that it
|
||||
// it 512K.
|
||||
const (
|
||||
maxPayload = 512 * 1024
|
||||
reqFixedOverhead = 100
|
||||
overheadPerID = 3
|
||||
maxSendRecvBytes = 20 * 1024 * 1024 // 20M
|
||||
)
|
||||
|
||||
func convertMessages(rms []*pb.ReceivedMessage) ([]*Message, error) {
|
||||
msgs := make([]*Message, 0, len(rms))
|
||||
for i, m := range rms {
|
||||
msg, err := toMessage(m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("pubsub: cannot decode the retrieved message at index: %d, message: %+v", i, m)
|
||||
}
|
||||
msgs = append(msgs, msg)
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
|
||||
func trunc32(i int64) int32 {
|
||||
if i > math.MaxInt32 {
|
||||
i = math.MaxInt32
|
||||
}
|
||||
return int32(i)
|
||||
}
|
||||
|
||||
// Logic from https://github.com/GoogleCloudPlatform/google-cloud-java/blob/master/google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/StatusUtil.java.
|
||||
func isRetryable(err error) bool {
|
||||
s, ok := status.FromError(err)
|
||||
if !ok { // includes io.EOF, normal stream close, which causes us to reopen
|
||||
return true
|
||||
}
|
||||
switch s.Code() {
|
||||
case codes.DeadlineExceeded, codes.Internal, codes.Canceled, codes.ResourceExhausted:
|
||||
return true
|
||||
case codes.Unavailable:
|
||||
return !strings.Contains(s.Message(), "Server shutdownNow invoked")
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Split req into a prefix that is smaller than maxSize, and a remainder.
|
||||
func splitRequest(req *pb.StreamingPullRequest, maxSize int) (prefix, remainder *pb.StreamingPullRequest) {
|
||||
const int32Bytes = 4
|
||||
|
||||
// Copy all fields before splitting the variable-sized ones.
|
||||
remainder = &pb.StreamingPullRequest{}
|
||||
*remainder = *req
|
||||
// Split message so it isn't too big.
|
||||
size := reqFixedOverhead
|
||||
i := 0
|
||||
for size < maxSize && (i < len(req.AckIds) || i < len(req.ModifyDeadlineAckIds)) {
|
||||
if i < len(req.AckIds) {
|
||||
size += overheadPerID + len(req.AckIds[i])
|
||||
}
|
||||
if i < len(req.ModifyDeadlineAckIds) {
|
||||
size += overheadPerID + len(req.ModifyDeadlineAckIds[i]) + int32Bytes
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
min := func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
j := i
|
||||
if size > maxSize {
|
||||
j--
|
||||
}
|
||||
k := min(j, len(req.AckIds))
|
||||
remainder.AckIds = req.AckIds[k:]
|
||||
req.AckIds = req.AckIds[:k]
|
||||
k = min(j, len(req.ModifyDeadlineAckIds))
|
||||
remainder.ModifyDeadlineAckIds = req.ModifyDeadlineAckIds[k:]
|
||||
remainder.ModifyDeadlineSeconds = req.ModifyDeadlineSeconds[k:]
|
||||
req.ModifyDeadlineAckIds = req.ModifyDeadlineAckIds[:k]
|
||||
req.ModifyDeadlineSeconds = req.ModifyDeadlineSeconds[:k]
|
||||
return req, remainder
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
func TestSplitRequest(t *testing.T) {
|
||||
split := func(a []string, i int) ([]string, []string) {
|
||||
if len(a) < i {
|
||||
return a, nil
|
||||
}
|
||||
return a[:i], a[i:]
|
||||
}
|
||||
ackIDs := []string{"aaaa", "bbbb", "cccc", "dddd", "eeee"}
|
||||
modDeadlines := []int32{1, 2, 3, 4, 5}
|
||||
for i, test := range []struct {
|
||||
ackIDs []string
|
||||
modAckIDs []string
|
||||
splitIndex int
|
||||
}{
|
||||
{ackIDs, ackIDs, 2},
|
||||
{nil, ackIDs, 3},
|
||||
{ackIDs, nil, 5},
|
||||
{nil, ackIDs[:1], 1},
|
||||
} {
|
||||
req := &pb.StreamingPullRequest{
|
||||
AckIds: test.ackIDs,
|
||||
ModifyDeadlineAckIds: test.modAckIDs,
|
||||
ModifyDeadlineSeconds: modDeadlines[:len(test.modAckIDs)],
|
||||
}
|
||||
a1, a2 := split(test.ackIDs, test.splitIndex)
|
||||
m1, m2 := split(test.modAckIDs, test.splitIndex)
|
||||
want1 := &pb.StreamingPullRequest{
|
||||
AckIds: a1,
|
||||
ModifyDeadlineAckIds: m1,
|
||||
ModifyDeadlineSeconds: modDeadlines[:len(m1)],
|
||||
}
|
||||
want2 := &pb.StreamingPullRequest{
|
||||
AckIds: a2,
|
||||
ModifyDeadlineAckIds: m2,
|
||||
ModifyDeadlineSeconds: modDeadlines[len(m1) : len(m1)+len(m2)],
|
||||
}
|
||||
got1, got2 := splitRequest(req, reqFixedOverhead+40)
|
||||
if !testutil.Equal(got1, want1) {
|
||||
t.Errorf("#%d: first:\ngot %+v\nwant %+v", i, got1, want1)
|
||||
}
|
||||
if !testutil.Equal(got2, want2) {
|
||||
t.Errorf("#%d: second:\ngot %+v\nwant %+v", i, got2, want2)
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
"golang.org/x/net/context"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
)
|
||||
|
||||
// Snapshot is a reference to a PubSub snapshot.
|
||||
type Snapshot struct {
|
||||
c *Client
|
||||
|
||||
// The fully qualified identifier for the snapshot, in the format "projects/<projid>/snapshots/<snap>"
|
||||
name string
|
||||
}
|
||||
|
||||
// ID returns the unique identifier of the snapshot within its project.
|
||||
func (s *Snapshot) ID() string {
|
||||
slash := strings.LastIndex(s.name, "/")
|
||||
if slash == -1 {
|
||||
// name is not a fully-qualified name.
|
||||
panic("bad snapshot name")
|
||||
}
|
||||
return s.name[slash+1:]
|
||||
}
|
||||
|
||||
// SnapshotConfig contains the details of a Snapshot.
|
||||
type SnapshotConfig struct {
|
||||
*Snapshot
|
||||
Topic *Topic
|
||||
Expiration time.Time
|
||||
}
|
||||
|
||||
// Snapshot creates a reference to a snapshot.
|
||||
func (c *Client) Snapshot(id string) *Snapshot {
|
||||
return &Snapshot{
|
||||
c: c,
|
||||
name: fmt.Sprintf("projects/%s/snapshots/%s", c.projectID, id),
|
||||
}
|
||||
}
|
||||
|
||||
// Snapshots returns an iterator which returns snapshots for this project.
|
||||
func (c *Client) Snapshots(ctx context.Context) *SnapshotConfigIterator {
|
||||
it := c.subc.ListSnapshots(ctx, &pb.ListSnapshotsRequest{
|
||||
Project: c.fullyQualifiedProjectName(),
|
||||
})
|
||||
next := func() (*SnapshotConfig, error) {
|
||||
snap, err := it.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toSnapshotConfig(snap, c)
|
||||
}
|
||||
return &SnapshotConfigIterator{next: next}
|
||||
}
|
||||
|
||||
// SnapshotConfigIterator is an iterator that returns a series of snapshots.
|
||||
type SnapshotConfigIterator struct {
|
||||
next func() (*SnapshotConfig, error)
|
||||
}
|
||||
|
||||
// Next returns the next SnapshotConfig. Its second return value is iterator.Done if there are no more results.
|
||||
// Once Next returns iterator.Done, all subsequent calls will return iterator.Done.
|
||||
func (snaps *SnapshotConfigIterator) Next() (*SnapshotConfig, error) {
|
||||
return snaps.next()
|
||||
}
|
||||
|
||||
// Delete deletes a snapshot.
|
||||
func (snap *Snapshot) Delete(ctx context.Context) error {
|
||||
return snap.c.subc.DeleteSnapshot(ctx, &pb.DeleteSnapshotRequest{Snapshot: snap.name})
|
||||
}
|
||||
|
||||
// SeekToTime seeks the subscription to a point in time.
|
||||
//
|
||||
// Messages retained in the subscription that were published before this
|
||||
// time are marked as acknowledged, and messages retained in the
|
||||
// subscription that were published after this time are marked as
|
||||
// unacknowledged. Note that this operation affects only those messages
|
||||
// retained in the subscription (configured by SnapshotConfig). For example,
|
||||
// if `time` corresponds to a point before the message retention
|
||||
// window (or to a point before the system's notion of the subscription
|
||||
// creation time), only retained messages will be marked as unacknowledged,
|
||||
// and already-expunged messages will not be restored.
|
||||
func (s *Subscription) SeekToTime(ctx context.Context, t time.Time) error {
|
||||
ts, err := ptypes.TimestampProto(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.c.subc.Seek(ctx, &pb.SeekRequest{
|
||||
Subscription: s.name,
|
||||
Target: &pb.SeekRequest_Time{ts},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateSnapshot creates a new snapshot from this subscription.
|
||||
// The snapshot will be for the topic this subscription is subscribed to.
|
||||
// If the name is empty string, a unique name is assigned.
|
||||
//
|
||||
// The created snapshot is guaranteed to retain:
|
||||
// (a) The existing backlog on the subscription. More precisely, this is
|
||||
// defined as the messages in the subscription's backlog that are
|
||||
// unacknowledged when Snapshot returns without error.
|
||||
// (b) Any messages published to the subscription's topic following
|
||||
// Snapshot returning without error.
|
||||
func (s *Subscription) CreateSnapshot(ctx context.Context, name string) (*SnapshotConfig, error) {
|
||||
if name != "" {
|
||||
name = fmt.Sprintf("projects/%s/snapshots/%s", strings.Split(s.name, "/")[1], name)
|
||||
}
|
||||
snap, err := s.c.subc.CreateSnapshot(ctx, &pb.CreateSnapshotRequest{
|
||||
Name: name,
|
||||
Subscription: s.name,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toSnapshotConfig(snap, s.c)
|
||||
}
|
||||
|
||||
// SeekToSnapshot seeks the subscription to a snapshot.
|
||||
//
|
||||
// The snapshot need not be created from this subscription,
|
||||
// but it must be for the topic this subscription is subscribed to.
|
||||
func (s *Subscription) SeekToSnapshot(ctx context.Context, snap *Snapshot) error {
|
||||
_, err := s.c.subc.Seek(ctx, &pb.SeekRequest{
|
||||
Subscription: s.name,
|
||||
Target: &pb.SeekRequest_Snapshot{snap.name},
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func toSnapshotConfig(snap *pb.Snapshot, c *Client) (*SnapshotConfig, error) {
|
||||
exp, err := ptypes.Timestamp(snap.ExpireTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &SnapshotConfig{
|
||||
Snapshot: &Snapshot{c: c, name: snap.Name},
|
||||
Topic: newTopic(c, snap.Topic),
|
||||
Expiration: exp,
|
||||
}, nil
|
||||
}
|
||||
+325
@@ -0,0 +1,325 @@
|
||||
// Copyright 2017 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
// TODO(jba): test keepalive
|
||||
// TODO(jba): test that expired messages are not kept alive
|
||||
// TODO(jba): test that when all messages expire, Stop returns.
|
||||
|
||||
import (
|
||||
"io"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
tspb "github.com/golang/protobuf/ptypes/timestamp"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/option"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var (
|
||||
timestamp = &tspb.Timestamp{}
|
||||
testMessages = []*pb.ReceivedMessage{
|
||||
{AckId: "0", Message: &pb.PubsubMessage{Data: []byte{1}, PublishTime: timestamp}},
|
||||
{AckId: "1", Message: &pb.PubsubMessage{Data: []byte{2}, PublishTime: timestamp}},
|
||||
{AckId: "2", Message: &pb.PubsubMessage{Data: []byte{3}, PublishTime: timestamp}},
|
||||
}
|
||||
)
|
||||
|
||||
func TestStreamingPullBasic(t *testing.T) {
|
||||
client, server := newFake(t)
|
||||
server.addStreamingPullMessages(testMessages)
|
||||
testStreamingPullIteration(t, client, server, testMessages)
|
||||
}
|
||||
|
||||
func TestStreamingPullMultipleFetches(t *testing.T) {
|
||||
client, server := newFake(t)
|
||||
server.addStreamingPullMessages(testMessages[:1])
|
||||
server.addStreamingPullMessages(testMessages[1:])
|
||||
testStreamingPullIteration(t, client, server, testMessages)
|
||||
}
|
||||
|
||||
func newTestSubscription(t *testing.T, client *Client, name string) *Subscription {
|
||||
topic := client.Topic("t")
|
||||
sub, err := client.CreateSubscription(context.Background(), name,
|
||||
SubscriptionConfig{Topic: topic})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateSubscription: %v", err)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
func testStreamingPullIteration(t *testing.T, client *Client, server *fakeServer, msgs []*pb.ReceivedMessage) {
|
||||
sub := newTestSubscription(t, client, "s")
|
||||
gotMsgs, err := pullN(context.Background(), sub, len(msgs), func(_ context.Context, m *Message) {
|
||||
id, err := strconv.Atoi(m.ackID)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// ack evens, nack odds
|
||||
if id%2 == 0 {
|
||||
m.Ack()
|
||||
} else {
|
||||
m.Nack()
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Pull: %v", err)
|
||||
}
|
||||
gotMap := map[string]*Message{}
|
||||
for _, m := range gotMsgs {
|
||||
gotMap[m.ackID] = m
|
||||
}
|
||||
for i, msg := range msgs {
|
||||
want, err := toMessage(msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want.calledDone = true
|
||||
got := gotMap[want.ackID]
|
||||
if got == nil {
|
||||
t.Errorf("%d: no message for ackID %q", i, want.ackID)
|
||||
continue
|
||||
}
|
||||
if !testutil.Equal(got, want, cmp.AllowUnexported(Message{}), cmpopts.IgnoreTypes(time.Time{}, func(string, bool, time.Time) {})) {
|
||||
t.Errorf("%d: got\n%#v\nwant\n%#v", i, got, want)
|
||||
}
|
||||
}
|
||||
server.wait()
|
||||
for i := 0; i < len(msgs); i++ {
|
||||
id := msgs[i].AckId
|
||||
if i%2 == 0 {
|
||||
if !server.Acked[id] {
|
||||
t.Errorf("msg %q should have been acked but wasn't", id)
|
||||
}
|
||||
} else {
|
||||
if dl, ok := server.Deadlines[id]; !ok || dl != 0 {
|
||||
t.Errorf("msg %q should have been nacked but wasn't", id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingPullError(t *testing.T) {
|
||||
// If an RPC to the service returns a non-retryable error, Pull should
|
||||
// return after all callbacks return, without waiting for messages to be
|
||||
// acked.
|
||||
client, server := newFake(t)
|
||||
server.addStreamingPullMessages(testMessages[:1])
|
||||
server.addStreamingPullError(status.Errorf(codes.Unknown, ""))
|
||||
sub := newTestSubscription(t, client, "s")
|
||||
// Use only one goroutine, since the fake server is configured to
|
||||
// return only one error.
|
||||
sub.ReceiveSettings.NumGoroutines = 1
|
||||
callbackDone := make(chan struct{})
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Second)
|
||||
err := sub.Receive(ctx, func(ctx context.Context, m *Message) {
|
||||
defer close(callbackDone)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
})
|
||||
select {
|
||||
case <-callbackDone:
|
||||
default:
|
||||
t.Fatal("Receive returned but callback was not done")
|
||||
}
|
||||
if want := codes.Unknown; grpc.Code(err) != want {
|
||||
t.Fatalf("got <%v>, want code %v", err, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingPullCancel(t *testing.T) {
|
||||
// If Receive's context is canceled, it should return after all callbacks
|
||||
// return and all messages have been acked.
|
||||
client, server := newFake(t)
|
||||
server.addStreamingPullMessages(testMessages)
|
||||
sub := newTestSubscription(t, client, "s")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
var n int32
|
||||
err := sub.Receive(ctx, func(ctx2 context.Context, m *Message) {
|
||||
atomic.AddInt32(&n, 1)
|
||||
defer atomic.AddInt32(&n, -1)
|
||||
cancel()
|
||||
m.Ack()
|
||||
})
|
||||
if got := atomic.LoadInt32(&n); got != 0 {
|
||||
t.Errorf("Receive returned with %d callbacks still running", got)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("Receive got <%v>, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingPullRetry(t *testing.T) {
|
||||
// Check that we retry on io.EOF or Unavailable.
|
||||
client, server := newFake(t)
|
||||
server.addStreamingPullMessages(testMessages[:1])
|
||||
server.addStreamingPullError(io.EOF)
|
||||
server.addStreamingPullError(io.EOF)
|
||||
server.addStreamingPullMessages(testMessages[1:2])
|
||||
server.addStreamingPullError(status.Errorf(codes.Unavailable, ""))
|
||||
server.addStreamingPullError(status.Errorf(codes.Unavailable, ""))
|
||||
server.addStreamingPullMessages(testMessages[2:])
|
||||
|
||||
testStreamingPullIteration(t, client, server, testMessages)
|
||||
}
|
||||
|
||||
func TestStreamingPullOneActive(t *testing.T) {
|
||||
// Only one call to Pull can be active at a time.
|
||||
client, srv := newFake(t)
|
||||
srv.addStreamingPullMessages(testMessages[:1])
|
||||
sub := newTestSubscription(t, client, "s")
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
err := sub.Receive(ctx, func(ctx context.Context, m *Message) {
|
||||
m.Ack()
|
||||
err := sub.Receive(ctx, func(context.Context, *Message) {})
|
||||
if err != errReceiveInProgress {
|
||||
t.Errorf("got <%v>, want <%v>", err, errReceiveInProgress)
|
||||
}
|
||||
cancel()
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("got <%v>, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingPullConcurrent(t *testing.T) {
|
||||
newMsg := func(i int) *pb.ReceivedMessage {
|
||||
return &pb.ReceivedMessage{
|
||||
AckId: strconv.Itoa(i),
|
||||
Message: &pb.PubsubMessage{Data: []byte{byte(i)}, PublishTime: timestamp},
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple goroutines should be able to read from the same iterator.
|
||||
client, server := newFake(t)
|
||||
// Add a lot of messages, a few at a time, to make sure both threads get a chance.
|
||||
nMessages := 100
|
||||
for i := 0; i < nMessages; i += 2 {
|
||||
server.addStreamingPullMessages([]*pb.ReceivedMessage{newMsg(i), newMsg(i + 1)})
|
||||
}
|
||||
sub := newTestSubscription(t, client, "s")
|
||||
ctx, _ := context.WithTimeout(context.Background(), time.Second)
|
||||
gotMsgs, err := pullN(ctx, sub, nMessages, func(ctx context.Context, m *Message) {
|
||||
m.Ack()
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Receive: %v", err)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, gm := range gotMsgs {
|
||||
if seen[gm.ackID] {
|
||||
t.Fatalf("duplicate ID %q", gm.ackID)
|
||||
}
|
||||
seen[gm.ackID] = true
|
||||
}
|
||||
if len(seen) != nMessages {
|
||||
t.Fatalf("got %d messages, want %d", len(seen), nMessages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStreamingPullFlowControl(t *testing.T) {
|
||||
// Callback invocations should not occur if flow control limits are exceeded.
|
||||
client, server := newFake(t)
|
||||
server.addStreamingPullMessages(testMessages)
|
||||
sub := newTestSubscription(t, client, "s")
|
||||
sub.ReceiveSettings.MaxOutstandingMessages = 2
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
activec := make(chan int)
|
||||
waitc := make(chan int)
|
||||
errc := make(chan error)
|
||||
go func() {
|
||||
errc <- sub.Receive(ctx, func(_ context.Context, m *Message) {
|
||||
activec <- 1
|
||||
<-waitc
|
||||
m.Ack()
|
||||
})
|
||||
}()
|
||||
// Here, two callbacks are active. Receive should be blocked in the flow
|
||||
// control acquire method on the third message.
|
||||
<-activec
|
||||
<-activec
|
||||
select {
|
||||
case <-activec:
|
||||
t.Fatal("third callback in progress")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
cancel()
|
||||
// Receive still has not returned, because both callbacks are still blocked on waitc.
|
||||
select {
|
||||
case err := <-errc:
|
||||
t.Fatalf("Receive returned early with error %v", err)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
// Let both callbacks proceed.
|
||||
waitc <- 1
|
||||
waitc <- 1
|
||||
// The third callback will never run, because acquire returned a non-nil
|
||||
// error, causing Receive to return. So now Receive should end.
|
||||
if err := <-errc; err != nil {
|
||||
t.Fatalf("got %v from Receive, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func newFake(t *testing.T) (*Client, *fakeServer) {
|
||||
srv, err := newFakeServer()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
client, err := NewClient(context.Background(), "projectID", option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return client, srv
|
||||
}
|
||||
|
||||
// pullN calls sub.Receive until at least n messages are received.
|
||||
func pullN(ctx context.Context, sub *Subscription, n int, f func(context.Context, *Message)) ([]*Message, error) {
|
||||
var (
|
||||
mu sync.Mutex
|
||||
msgs []*Message
|
||||
)
|
||||
cctx, cancel := context.WithCancel(ctx)
|
||||
err := sub.Receive(cctx, func(ctx context.Context, m *Message) {
|
||||
mu.Lock()
|
||||
msgs = append(msgs, m)
|
||||
nSeen := len(msgs)
|
||||
mu.Unlock()
|
||||
f(ctx, m)
|
||||
if nSeen >= n {
|
||||
cancel()
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return msgs, nil
|
||||
}
|
||||
+522
@@ -0,0 +1,522 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/iam"
|
||||
"cloud.google.com/go/internal/optional"
|
||||
"github.com/golang/protobuf/ptypes"
|
||||
durpb "github.com/golang/protobuf/ptypes/duration"
|
||||
"golang.org/x/net/context"
|
||||
"golang.org/x/sync/errgroup"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
fmpb "google.golang.org/genproto/protobuf/field_mask"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
// Subscription is a reference to a PubSub subscription.
|
||||
type Subscription struct {
|
||||
c *Client
|
||||
|
||||
// The fully qualified identifier for the subscription, in the format "projects/<projid>/subscriptions/<name>"
|
||||
name string
|
||||
|
||||
// Settings for pulling messages. Configure these before calling Receive.
|
||||
ReceiveSettings ReceiveSettings
|
||||
|
||||
mu sync.Mutex
|
||||
receiveActive bool
|
||||
}
|
||||
|
||||
// Subscription creates a reference to a subscription.
|
||||
func (c *Client) Subscription(id string) *Subscription {
|
||||
return c.SubscriptionInProject(id, c.projectID)
|
||||
}
|
||||
|
||||
// SubscriptionInProject creates a reference to a subscription in a given project.
|
||||
func (c *Client) SubscriptionInProject(id, projectID string) *Subscription {
|
||||
return &Subscription{
|
||||
c: c,
|
||||
name: fmt.Sprintf("projects/%s/subscriptions/%s", projectID, id),
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the globally unique printable name of the subscription.
|
||||
func (s *Subscription) String() string {
|
||||
return s.name
|
||||
}
|
||||
|
||||
// ID returns the unique identifier of the subscription within its project.
|
||||
func (s *Subscription) ID() string {
|
||||
slash := strings.LastIndex(s.name, "/")
|
||||
if slash == -1 {
|
||||
// name is not a fully-qualified name.
|
||||
panic("bad subscription name")
|
||||
}
|
||||
return s.name[slash+1:]
|
||||
}
|
||||
|
||||
// Subscriptions returns an iterator which returns all of the subscriptions for the client's project.
|
||||
func (c *Client) Subscriptions(ctx context.Context) *SubscriptionIterator {
|
||||
it := c.subc.ListSubscriptions(ctx, &pb.ListSubscriptionsRequest{
|
||||
Project: c.fullyQualifiedProjectName(),
|
||||
})
|
||||
return &SubscriptionIterator{
|
||||
c: c,
|
||||
next: func() (string, error) {
|
||||
sub, err := it.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return sub.Name, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SubscriptionIterator is an iterator that returns a series of subscriptions.
|
||||
type SubscriptionIterator struct {
|
||||
c *Client
|
||||
next func() (string, error)
|
||||
}
|
||||
|
||||
// Next returns the next subscription. If there are no more subscriptions, iterator.Done will be returned.
|
||||
func (subs *SubscriptionIterator) Next() (*Subscription, error) {
|
||||
subName, err := subs.next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Subscription{c: subs.c, name: subName}, nil
|
||||
}
|
||||
|
||||
// PushConfig contains configuration for subscriptions that operate in push mode.
|
||||
type PushConfig struct {
|
||||
// A URL locating the endpoint to which messages should be pushed.
|
||||
Endpoint string
|
||||
|
||||
// Endpoint configuration attributes. See https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.subscriptions#pushconfig for more details.
|
||||
Attributes map[string]string
|
||||
}
|
||||
|
||||
func (pc *PushConfig) toProto() *pb.PushConfig {
|
||||
return &pb.PushConfig{
|
||||
Attributes: pc.Attributes,
|
||||
PushEndpoint: pc.Endpoint,
|
||||
}
|
||||
}
|
||||
|
||||
// Subscription config contains the configuration of a subscription.
|
||||
type SubscriptionConfig struct {
|
||||
Topic *Topic
|
||||
PushConfig PushConfig
|
||||
|
||||
// The default maximum time after a subscriber receives a message before
|
||||
// the subscriber should acknowledge the message. Note: messages which are
|
||||
// obtained via Subscription.Receive need not be acknowledged within this
|
||||
// deadline, as the deadline will be automatically extended.
|
||||
AckDeadline time.Duration
|
||||
|
||||
// Whether to retain acknowledged messages. If true, acknowledged messages
|
||||
// will not be expunged until they fall out of the RetentionDuration window.
|
||||
RetainAckedMessages bool
|
||||
|
||||
// How long to retain messages in backlog, from the time of publish. If
|
||||
// RetainAckedMessages is true, this duration affects the retention of
|
||||
// acknowledged messages, otherwise only unacknowledged messages are retained.
|
||||
// Defaults to 7 days. Cannot be longer than 7 days or shorter than 10 minutes.
|
||||
RetentionDuration time.Duration
|
||||
}
|
||||
|
||||
func (cfg *SubscriptionConfig) toProto(name string) *pb.Subscription {
|
||||
var pbPushConfig *pb.PushConfig
|
||||
if cfg.PushConfig.Endpoint != "" || len(cfg.PushConfig.Attributes) != 0 {
|
||||
pbPushConfig = &pb.PushConfig{
|
||||
Attributes: cfg.PushConfig.Attributes,
|
||||
PushEndpoint: cfg.PushConfig.Endpoint,
|
||||
}
|
||||
}
|
||||
var retentionDuration *durpb.Duration
|
||||
if cfg.RetentionDuration != 0 {
|
||||
retentionDuration = ptypes.DurationProto(cfg.RetentionDuration)
|
||||
}
|
||||
return &pb.Subscription{
|
||||
Name: name,
|
||||
Topic: cfg.Topic.name,
|
||||
PushConfig: pbPushConfig,
|
||||
AckDeadlineSeconds: trunc32(int64(cfg.AckDeadline.Seconds())),
|
||||
RetainAckedMessages: cfg.RetainAckedMessages,
|
||||
MessageRetentionDuration: retentionDuration,
|
||||
}
|
||||
}
|
||||
|
||||
func protoToSubscriptionConfig(pbSub *pb.Subscription, c *Client) (SubscriptionConfig, error) {
|
||||
rd := time.Hour * 24 * 7
|
||||
var err error
|
||||
if pbSub.MessageRetentionDuration != nil {
|
||||
rd, err = ptypes.Duration(pbSub.MessageRetentionDuration)
|
||||
if err != nil {
|
||||
return SubscriptionConfig{}, err
|
||||
}
|
||||
}
|
||||
return SubscriptionConfig{
|
||||
Topic: newTopic(c, pbSub.Topic),
|
||||
AckDeadline: time.Second * time.Duration(pbSub.AckDeadlineSeconds),
|
||||
PushConfig: PushConfig{
|
||||
Endpoint: pbSub.PushConfig.PushEndpoint,
|
||||
Attributes: pbSub.PushConfig.Attributes,
|
||||
},
|
||||
RetainAckedMessages: pbSub.RetainAckedMessages,
|
||||
RetentionDuration: rd,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ReceiveSettings configure the Receive method.
|
||||
// A zero ReceiveSettings will result in values equivalent to DefaultReceiveSettings.
|
||||
type ReceiveSettings struct {
|
||||
// MaxExtension is the maximum period for which the Subscription should
|
||||
// automatically extend the ack deadline for each message.
|
||||
//
|
||||
// The Subscription will automatically extend the ack deadline of all
|
||||
// fetched Messages for the duration specified. Automatic deadline
|
||||
// extension may be disabled by specifying a duration less than 0.
|
||||
//
|
||||
// Connections may be terminated if they last longer than 30m, which
|
||||
// effectively makes that the ceiling for this value. For longer message
|
||||
// processing, see the example at https://godoc.org/cloud.google.com/go/pubsub/apiv1#example_SubscriberClient_Pull_lengthyClientProcessing
|
||||
MaxExtension time.Duration
|
||||
|
||||
// MaxOutstandingMessages is the maximum number of unprocessed messages
|
||||
// (unacknowledged but not yet expired). If MaxOutstandingMessages is 0, it
|
||||
// will be treated as if it were DefaultReceiveSettings.MaxOutstandingMessages.
|
||||
// If the value is negative, then there will be no limit on the number of
|
||||
// unprocessed messages.
|
||||
MaxOutstandingMessages int
|
||||
|
||||
// MaxOutstandingBytes is the maximum size of unprocessed messages
|
||||
// (unacknowledged but not yet expired). If MaxOutstandingBytes is 0, it will
|
||||
// be treated as if it were DefaultReceiveSettings.MaxOutstandingBytes. If
|
||||
// the value is negative, then there will be no limit on the number of bytes
|
||||
// for unprocessed messages.
|
||||
MaxOutstandingBytes int
|
||||
|
||||
// NumGoroutines is the number of goroutines Receive will spawn to pull
|
||||
// messages concurrently. If NumGoroutines is less than 1, it will be treated
|
||||
// as if it were DefaultReceiveSettings.NumGoroutines.
|
||||
//
|
||||
// NumGoroutines does not limit the number of messages that can be processed
|
||||
// concurrently. Even with one goroutine, many messages might be processed at
|
||||
// once, because that goroutine may continually receive messages and invoke the
|
||||
// function passed to Receive on them. To limit the number of messages being
|
||||
// processed concurrently, set MaxOutstandingMessages.
|
||||
NumGoroutines int
|
||||
}
|
||||
|
||||
// DefaultReceiveSettings holds the default values for ReceiveSettings.
|
||||
var DefaultReceiveSettings = ReceiveSettings{
|
||||
MaxExtension: 10 * time.Minute,
|
||||
MaxOutstandingMessages: 1000,
|
||||
MaxOutstandingBytes: 1e9, // 1G
|
||||
NumGoroutines: 1,
|
||||
}
|
||||
|
||||
// Delete deletes the subscription.
|
||||
func (s *Subscription) Delete(ctx context.Context) error {
|
||||
return s.c.subc.DeleteSubscription(ctx, &pb.DeleteSubscriptionRequest{Subscription: s.name})
|
||||
}
|
||||
|
||||
// Exists reports whether the subscription exists on the server.
|
||||
func (s *Subscription) Exists(ctx context.Context) (bool, error) {
|
||||
_, err := s.c.subc.GetSubscription(ctx, &pb.GetSubscriptionRequest{Subscription: s.name})
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if grpc.Code(err) == codes.NotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Config fetches the current configuration for the subscription.
|
||||
func (s *Subscription) Config(ctx context.Context) (SubscriptionConfig, error) {
|
||||
pbSub, err := s.c.subc.GetSubscription(ctx, &pb.GetSubscriptionRequest{Subscription: s.name})
|
||||
if err != nil {
|
||||
return SubscriptionConfig{}, err
|
||||
}
|
||||
cfg, err := protoToSubscriptionConfig(pbSub, s.c)
|
||||
if err != nil {
|
||||
return SubscriptionConfig{}, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// SubscriptionConfigToUpdate describes how to update a subscription.
|
||||
type SubscriptionConfigToUpdate struct {
|
||||
// If non-nil, the push config is changed.
|
||||
PushConfig *PushConfig
|
||||
|
||||
// If non-zero, the ack deadline is changed.
|
||||
AckDeadline time.Duration
|
||||
|
||||
// If set, RetainAckedMessages is changed.
|
||||
RetainAckedMessages optional.Bool
|
||||
|
||||
// If non-zero, RetentionDuration is changed.
|
||||
RetentionDuration time.Duration
|
||||
}
|
||||
|
||||
// Update changes an existing subscription according to the fields set in cfg.
|
||||
// It returns the new SubscriptionConfig.
|
||||
//
|
||||
// Update returns an error if no fields were modified.
|
||||
func (s *Subscription) Update(ctx context.Context, cfg SubscriptionConfigToUpdate) (SubscriptionConfig, error) {
|
||||
req := s.updateRequest(&cfg)
|
||||
if len(req.UpdateMask.Paths) == 0 {
|
||||
return SubscriptionConfig{}, errors.New("pubsub: UpdateSubscription call with nothing to update")
|
||||
}
|
||||
rpsub, err := s.c.subc.UpdateSubscription(ctx, req)
|
||||
if err != nil {
|
||||
return SubscriptionConfig{}, err
|
||||
}
|
||||
return protoToSubscriptionConfig(rpsub, s.c)
|
||||
}
|
||||
|
||||
func (s *Subscription) updateRequest(cfg *SubscriptionConfigToUpdate) *pb.UpdateSubscriptionRequest {
|
||||
psub := &pb.Subscription{Name: s.name}
|
||||
var paths []string
|
||||
if cfg.PushConfig != nil {
|
||||
psub.PushConfig = cfg.PushConfig.toProto()
|
||||
paths = append(paths, "push_config")
|
||||
}
|
||||
if cfg.AckDeadline != 0 {
|
||||
psub.AckDeadlineSeconds = trunc32(int64(cfg.AckDeadline.Seconds()))
|
||||
paths = append(paths, "ack_deadline_seconds")
|
||||
}
|
||||
if cfg.RetainAckedMessages != nil {
|
||||
psub.RetainAckedMessages = optional.ToBool(cfg.RetainAckedMessages)
|
||||
paths = append(paths, "retain_acked_messages")
|
||||
}
|
||||
if cfg.RetentionDuration != 0 {
|
||||
psub.MessageRetentionDuration = ptypes.DurationProto(cfg.RetentionDuration)
|
||||
paths = append(paths, "message_retention_duration")
|
||||
}
|
||||
return &pb.UpdateSubscriptionRequest{
|
||||
Subscription: psub,
|
||||
UpdateMask: &fmpb.FieldMask{Paths: paths},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Subscription) IAM() *iam.Handle {
|
||||
return iam.InternalNewHandle(s.c.subc.Connection(), s.name)
|
||||
}
|
||||
|
||||
// CreateSubscription creates a new subscription on a topic.
|
||||
//
|
||||
// id is the name of the subscription to create. It must start with a letter,
|
||||
// and contain only letters ([A-Za-z]), numbers ([0-9]), dashes (-),
|
||||
// underscores (_), periods (.), tildes (~), plus (+) or percent signs (%). It
|
||||
// must be between 3 and 255 characters in length, and must not start with
|
||||
// "goog".
|
||||
//
|
||||
// cfg.Topic is the topic from which the subscription should receive messages. It
|
||||
// need not belong to the same project as the subscription. This field is required.
|
||||
//
|
||||
// cfg.AckDeadline is the maximum time after a subscriber receives a message before
|
||||
// the subscriber should acknowledge the message. It must be between 10 and 600
|
||||
// seconds (inclusive), and is rounded down to the nearest second. If the
|
||||
// provided ackDeadline is 0, then the default value of 10 seconds is used.
|
||||
// Note: messages which are obtained via Subscription.Receive need not be
|
||||
// acknowledged within this deadline, as the deadline will be automatically
|
||||
// extended.
|
||||
//
|
||||
// cfg.PushConfig may be set to configure this subscription for push delivery.
|
||||
//
|
||||
// If the subscription already exists an error will be returned.
|
||||
func (c *Client) CreateSubscription(ctx context.Context, id string, cfg SubscriptionConfig) (*Subscription, error) {
|
||||
if cfg.Topic == nil {
|
||||
return nil, errors.New("pubsub: require non-nil Topic")
|
||||
}
|
||||
if cfg.AckDeadline == 0 {
|
||||
cfg.AckDeadline = 10 * time.Second
|
||||
}
|
||||
if d := cfg.AckDeadline; d < 10*time.Second || d > 600*time.Second {
|
||||
return nil, fmt.Errorf("ack deadline must be between 10 and 600 seconds; got: %v", d)
|
||||
}
|
||||
|
||||
sub := c.Subscription(id)
|
||||
_, err := c.subc.CreateSubscription(ctx, cfg.toProto(sub.name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sub, nil
|
||||
}
|
||||
|
||||
var errReceiveInProgress = errors.New("pubsub: Receive already in progress for this subscription")
|
||||
|
||||
// Receive calls f with the outstanding messages from the subscription.
|
||||
// It blocks until ctx is done, or the service returns a non-retryable error.
|
||||
//
|
||||
// The standard way to terminate a Receive is to cancel its context:
|
||||
//
|
||||
// cctx, cancel := context.WithCancel(ctx)
|
||||
// err := sub.Receive(cctx, callback)
|
||||
// // Call cancel from callback, or another goroutine.
|
||||
//
|
||||
// If the service returns a non-retryable error, Receive returns that error after
|
||||
// all of the outstanding calls to f have returned. If ctx is done, Receive
|
||||
// returns nil after all of the outstanding calls to f have returned and
|
||||
// all messages have been acknowledged or have expired.
|
||||
//
|
||||
// Receive calls f concurrently from multiple goroutines. It is encouraged to
|
||||
// process messages synchronously in f, even if that processing is relatively
|
||||
// time-consuming; Receive will spawn new goroutines for incoming messages,
|
||||
// limited by MaxOutstandingMessages and MaxOutstandingBytes in ReceiveSettings.
|
||||
//
|
||||
// The context passed to f will be canceled when ctx is Done or there is a
|
||||
// fatal service error.
|
||||
//
|
||||
// Receive will automatically extend the ack deadline of all fetched Messages for the
|
||||
// period specified by s.ReceiveSettings.MaxExtension.
|
||||
//
|
||||
// Each Subscription may have only one invocation of Receive active at a time.
|
||||
func (s *Subscription) Receive(ctx context.Context, f func(context.Context, *Message)) error {
|
||||
s.mu.Lock()
|
||||
if s.receiveActive {
|
||||
s.mu.Unlock()
|
||||
return errReceiveInProgress
|
||||
}
|
||||
s.receiveActive = true
|
||||
s.mu.Unlock()
|
||||
defer func() { s.mu.Lock(); s.receiveActive = false; s.mu.Unlock() }()
|
||||
|
||||
config, err := s.Config(ctx)
|
||||
if err != nil {
|
||||
if grpc.Code(err) == codes.Canceled {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
maxCount := s.ReceiveSettings.MaxOutstandingMessages
|
||||
if maxCount == 0 {
|
||||
maxCount = DefaultReceiveSettings.MaxOutstandingMessages
|
||||
}
|
||||
maxBytes := s.ReceiveSettings.MaxOutstandingBytes
|
||||
if maxBytes == 0 {
|
||||
maxBytes = DefaultReceiveSettings.MaxOutstandingBytes
|
||||
}
|
||||
maxExt := s.ReceiveSettings.MaxExtension
|
||||
if maxExt == 0 {
|
||||
maxExt = DefaultReceiveSettings.MaxExtension
|
||||
} else if maxExt < 0 {
|
||||
// If MaxExtension is negative, disable automatic extension.
|
||||
maxExt = 0
|
||||
}
|
||||
numGoroutines := s.ReceiveSettings.NumGoroutines
|
||||
if numGoroutines < 1 {
|
||||
numGoroutines = DefaultReceiveSettings.NumGoroutines
|
||||
}
|
||||
// TODO(jba): add tests that verify that ReceiveSettings are correctly processed.
|
||||
po := &pullOptions{
|
||||
maxExtension: maxExt,
|
||||
maxPrefetch: trunc32(int64(maxCount)),
|
||||
ackDeadline: config.AckDeadline,
|
||||
}
|
||||
fc := newFlowController(maxCount, maxBytes)
|
||||
|
||||
// Wait for all goroutines started by Receive to return, so instead of an
|
||||
// obscure goroutine leak we have an obvious blocked call to Receive.
|
||||
group, gctx := errgroup.WithContext(ctx)
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
group.Go(func() error {
|
||||
return s.receive(gctx, po, fc, f)
|
||||
})
|
||||
}
|
||||
return group.Wait()
|
||||
}
|
||||
|
||||
func (s *Subscription) receive(ctx context.Context, po *pullOptions, fc *flowController, f func(context.Context, *Message)) error {
|
||||
// Cancel a sub-context when we return, to kick the context-aware callbacks
|
||||
// and the goroutine below.
|
||||
ctx2, cancel := context.WithCancel(ctx)
|
||||
// Call stop when Receive's context is done.
|
||||
// Stop will block until all outstanding messages have been acknowledged
|
||||
// or there was a fatal service error.
|
||||
// The iterator does not use the context passed to Receive. If it did, canceling
|
||||
// that context would immediately stop the iterator without waiting for unacked
|
||||
// messages.
|
||||
iter := newMessageIterator(context.Background(), s.c.subc, s.name, po)
|
||||
|
||||
// We cannot use errgroup from Receive here. Receive might already be calling group.Wait,
|
||||
// and group.Wait cannot be called concurrently with group.Go. We give each receive() its
|
||||
// own WaitGroup instead.
|
||||
// Since wg.Add is only called from the main goroutine, wg.Wait is guaranteed
|
||||
// to be called after all Adds.
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
<-ctx2.Done()
|
||||
iter.stop()
|
||||
wg.Done()
|
||||
}()
|
||||
defer wg.Wait()
|
||||
|
||||
defer cancel()
|
||||
for {
|
||||
msgs, err := iter.receive()
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i, msg := range msgs {
|
||||
msg := msg
|
||||
// TODO(jba): call acquire closer to when the message is allocated.
|
||||
if err := fc.acquire(ctx, len(msg.Data)); err != nil {
|
||||
// TODO(jba): test that these "orphaned" messages are nacked immediately when ctx is done.
|
||||
for _, m := range msgs[i:] {
|
||||
m.Nack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
old := msg.doneFunc
|
||||
msgLen := len(msg.Data)
|
||||
msg.doneFunc = func(ackID string, ack bool, receiveTime time.Time) {
|
||||
defer fc.release(msgLen)
|
||||
old(ackID, ack, receiveTime)
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
f(ctx2, msg)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(jba): remove when we delete messageIterator.
|
||||
type pullOptions struct {
|
||||
maxExtension time.Duration
|
||||
maxPrefetch int32
|
||||
// ackDeadline is the default ack deadline for the subscription. Not
|
||||
// configurable.
|
||||
ackDeadline time.Duration
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"google.golang.org/api/iterator"
|
||||
)
|
||||
|
||||
// All returns the remaining subscriptions from this iterator.
|
||||
func slurpSubs(it *SubscriptionIterator) ([]*Subscription, error) {
|
||||
var subs []*Subscription
|
||||
for {
|
||||
switch sub, err := it.Next(); err {
|
||||
case nil:
|
||||
subs = append(subs, sub)
|
||||
case iterator.Done:
|
||||
return subs, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscriptionID(t *testing.T) {
|
||||
const id = "id"
|
||||
c := &Client{projectID: "projid"}
|
||||
s := c.Subscription(id)
|
||||
if got, want := s.ID(), id; got != want {
|
||||
t.Errorf("Subscription.ID() = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListProjectSubscriptions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, _ := newFake(t)
|
||||
topic := mustCreateTopic(t, c, "t")
|
||||
var want []string
|
||||
for i := 1; i <= 2; i++ {
|
||||
id := fmt.Sprintf("s%d", i)
|
||||
want = append(want, id)
|
||||
_, err := c.CreateSubscription(ctx, id, SubscriptionConfig{Topic: topic})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
subs, err := slurpSubs(c.Subscriptions(ctx))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got := getSubIDs(subs)
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Errorf("got %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func getSubIDs(subs []*Subscription) []string {
|
||||
var names []string
|
||||
for _, sub := range subs {
|
||||
names = append(names, sub.ID())
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func TestListTopicSubscriptions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
c, _ := newFake(t)
|
||||
topics := []*Topic{
|
||||
mustCreateTopic(t, c, "t0"),
|
||||
mustCreateTopic(t, c, "t1"),
|
||||
}
|
||||
wants := make([][]string, 2)
|
||||
for i := 0; i < 5; i++ {
|
||||
id := fmt.Sprintf("s%d", i)
|
||||
sub, err := c.CreateSubscription(ctx, id, SubscriptionConfig{Topic: topics[i%2]})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wants[i%2] = append(wants[i%2], sub.ID())
|
||||
}
|
||||
|
||||
for i, topic := range topics {
|
||||
subs, err := slurpSubs(topic.Subscriptions(ctx))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := getSubIDs(subs)
|
||||
if !testutil.Equal(got, wants[i]) {
|
||||
t.Errorf("#%d: got %v, want %v", i, got, wants[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const defaultRetentionDuration = 168 * time.Hour
|
||||
|
||||
func TestUpdateSubscription(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client, _ := newFake(t)
|
||||
defer client.Close()
|
||||
|
||||
topic := client.Topic("t")
|
||||
sub, err := client.CreateSubscription(ctx, "s", SubscriptionConfig{Topic: topic})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := sub.Config(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := SubscriptionConfig{
|
||||
Topic: topic,
|
||||
AckDeadline: 10 * time.Second,
|
||||
RetainAckedMessages: false,
|
||||
RetentionDuration: defaultRetentionDuration,
|
||||
}
|
||||
if !testutil.Equal(cfg, want) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", cfg, want)
|
||||
}
|
||||
|
||||
got, err := sub.Update(ctx, SubscriptionConfigToUpdate{
|
||||
AckDeadline: 20 * time.Second,
|
||||
RetainAckedMessages: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want = SubscriptionConfig{
|
||||
Topic: topic,
|
||||
AckDeadline: 20 * time.Second,
|
||||
RetainAckedMessages: true,
|
||||
RetentionDuration: defaultRetentionDuration,
|
||||
}
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
|
||||
got, err = sub.Update(ctx, SubscriptionConfigToUpdate{RetentionDuration: 2 * time.Hour})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want.RetentionDuration = 2 * time.Hour
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Fatalf("\ngot %+v\nwant %+v", got, want)
|
||||
}
|
||||
|
||||
_, err = sub.Update(ctx, SubscriptionConfigToUpdate{})
|
||||
if err == nil {
|
||||
t.Fatal("got nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func (t1 *Topic) Equal(t2 *Topic) bool {
|
||||
if t1 == nil && t2 == nil {
|
||||
return true
|
||||
}
|
||||
if t1 == nil || t2 == nil {
|
||||
return false
|
||||
}
|
||||
return t1.c == t2.c && t1.name == t2.name
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
// Copyright 2018 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"cloud.google.com/go/pubsub/pstest"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
// Using the fake PubSub server in the pstest package, verify that streaming
|
||||
// pull resumes if the server stream times out.
|
||||
func TestStreamTimeout(t *testing.T) {
|
||||
log.SetFlags(log.Lmicroseconds)
|
||||
ctx := context.Background()
|
||||
srv := pstest.NewServer()
|
||||
srv.SetStreamTimeout(2 * time.Second)
|
||||
conn, err := grpc.Dial(srv.Addr, grpc.WithInsecure())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client, err := NewClient(ctx, "P", option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer client.Close()
|
||||
topic, err := client.CreateTopic(ctx, "T")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sub, err := client.CreateSubscription(ctx, "sub", SubscriptionConfig{Topic: topic, AckDeadline: 10 * time.Second})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
const nPublish = 8
|
||||
rctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
errc := make(chan error)
|
||||
var nSeen int64
|
||||
go func() {
|
||||
errc <- sub.Receive(rctx, func(ctx context.Context, m *Message) {
|
||||
m.Ack()
|
||||
n := atomic.AddInt64(&nSeen, 1)
|
||||
if n >= nPublish {
|
||||
cancel()
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
for i := 0; i < nPublish; i++ {
|
||||
pr := topic.Publish(ctx, &Message{Data: []byte("msg")})
|
||||
_, err := pr.Get(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
err = <-errc
|
||||
if err := sub.Delete(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n := atomic.LoadInt64(&nSeen)
|
||||
t.Logf("Receive returned %v after seeing %d messages\n", err, n)
|
||||
if n < nPublish {
|
||||
t.Errorf("got %d messages, want %d", n, nPublish)
|
||||
}
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/iam"
|
||||
"github.com/golang/protobuf/proto"
|
||||
gax "github.com/googleapis/gax-go"
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/support/bundler"
|
||||
pb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
const (
|
||||
// The maximum number of messages that can be in a single publish request, as
|
||||
// determined by the PubSub service.
|
||||
MaxPublishRequestCount = 1000
|
||||
|
||||
// The maximum size of a single publish request in bytes, as determined by the PubSub service.
|
||||
MaxPublishRequestBytes = 1e7
|
||||
|
||||
maxInt = int(^uint(0) >> 1)
|
||||
)
|
||||
|
||||
// ErrOversizedMessage indicates that a message's size exceeds MaxPublishRequestBytes.
|
||||
var ErrOversizedMessage = bundler.ErrOversizedItem
|
||||
|
||||
// Topic is a reference to a PubSub topic.
|
||||
//
|
||||
// The methods of Topic are safe for use by multiple goroutines.
|
||||
type Topic struct {
|
||||
c *Client
|
||||
// The fully qualified identifier for the topic, in the format "projects/<projid>/topics/<name>"
|
||||
name string
|
||||
|
||||
// Settings for publishing messages. All changes must be made before the
|
||||
// first call to Publish. The default is DefaultPublishSettings.
|
||||
PublishSettings PublishSettings
|
||||
|
||||
mu sync.RWMutex
|
||||
stopped bool
|
||||
bundler *bundler.Bundler
|
||||
|
||||
wg sync.WaitGroup
|
||||
|
||||
// Channel for message bundles to be published. Close to indicate that Stop was called.
|
||||
bundlec chan []*bundledMessage
|
||||
}
|
||||
|
||||
// PublishSettings control the bundling of published messages.
|
||||
type PublishSettings struct {
|
||||
|
||||
// Publish a non-empty batch after this delay has passed.
|
||||
DelayThreshold time.Duration
|
||||
|
||||
// Publish a batch when it has this many messages. The maximum is
|
||||
// MaxPublishRequestCount.
|
||||
CountThreshold int
|
||||
|
||||
// Publish a batch when its size in bytes reaches this value.
|
||||
ByteThreshold int
|
||||
|
||||
// The number of goroutines that invoke the Publish RPC concurrently.
|
||||
// Defaults to a multiple of GOMAXPROCS.
|
||||
NumGoroutines int
|
||||
|
||||
// The maximum time that the client will attempt to publish a bundle of messages.
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// DefaultPublishSettings holds the default values for topics' PublishSettings.
|
||||
var DefaultPublishSettings = PublishSettings{
|
||||
DelayThreshold: 1 * time.Millisecond,
|
||||
CountThreshold: 100,
|
||||
ByteThreshold: 1e6,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
// CreateTopic creates a new topic.
|
||||
// The specified topic ID must start with a letter, and contain only letters
|
||||
// ([A-Za-z]), numbers ([0-9]), dashes (-), underscores (_), periods (.),
|
||||
// tildes (~), plus (+) or percent signs (%). It must be between 3 and 255
|
||||
// characters in length, and must not start with "goog".
|
||||
// If the topic already exists an error will be returned.
|
||||
func (c *Client) CreateTopic(ctx context.Context, id string) (*Topic, error) {
|
||||
t := c.Topic(id)
|
||||
_, err := c.pubc.CreateTopic(ctx, &pb.Topic{Name: t.name})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Topic creates a reference to a topic in the client's project.
|
||||
//
|
||||
// If a Topic's Publish method is called, it has background goroutines
|
||||
// associated with it. Clean them up by calling Topic.Stop.
|
||||
//
|
||||
// Avoid creating many Topic instances if you use them to publish.
|
||||
func (c *Client) Topic(id string) *Topic {
|
||||
return c.TopicInProject(id, c.projectID)
|
||||
}
|
||||
|
||||
// TopicInProject creates a reference to a topic in the given project.
|
||||
//
|
||||
// If a Topic's Publish method is called, it has background goroutines
|
||||
// associated with it. Clean them up by calling Topic.Stop.
|
||||
//
|
||||
// Avoid creating many Topic instances if you use them to publish.
|
||||
func (c *Client) TopicInProject(id, projectID string) *Topic {
|
||||
return newTopic(c, fmt.Sprintf("projects/%s/topics/%s", projectID, id))
|
||||
}
|
||||
|
||||
func newTopic(c *Client, name string) *Topic {
|
||||
// bundlec is unbuffered. A buffer would occupy memory not
|
||||
// accounted for by the bundler, so BufferedByteLimit would be a lie:
|
||||
// the actual memory consumed would be higher.
|
||||
return &Topic{
|
||||
c: c,
|
||||
name: name,
|
||||
PublishSettings: DefaultPublishSettings,
|
||||
bundlec: make(chan []*bundledMessage),
|
||||
}
|
||||
}
|
||||
|
||||
// Topics returns an iterator which returns all of the topics for the client's project.
|
||||
func (c *Client) Topics(ctx context.Context) *TopicIterator {
|
||||
it := c.pubc.ListTopics(ctx, &pb.ListTopicsRequest{Project: c.fullyQualifiedProjectName()})
|
||||
return &TopicIterator{
|
||||
c: c,
|
||||
next: func() (string, error) {
|
||||
topic, err := it.Next()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return topic.Name, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TopicIterator is an iterator that returns a series of topics.
|
||||
type TopicIterator struct {
|
||||
c *Client
|
||||
next func() (string, error)
|
||||
}
|
||||
|
||||
// Next returns the next topic. If there are no more topics, iterator.Done will be returned.
|
||||
func (tps *TopicIterator) Next() (*Topic, error) {
|
||||
topicName, err := tps.next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newTopic(tps.c, topicName), nil
|
||||
}
|
||||
|
||||
// ID returns the unique idenfier of the topic within its project.
|
||||
func (t *Topic) ID() string {
|
||||
slash := strings.LastIndex(t.name, "/")
|
||||
if slash == -1 {
|
||||
// name is not a fully-qualified name.
|
||||
panic("bad topic name")
|
||||
}
|
||||
return t.name[slash+1:]
|
||||
}
|
||||
|
||||
// String returns the printable globally unique name for the topic.
|
||||
func (t *Topic) String() string {
|
||||
return t.name
|
||||
}
|
||||
|
||||
// Delete deletes the topic.
|
||||
func (t *Topic) Delete(ctx context.Context) error {
|
||||
return t.c.pubc.DeleteTopic(ctx, &pb.DeleteTopicRequest{Topic: t.name})
|
||||
}
|
||||
|
||||
// Exists reports whether the topic exists on the server.
|
||||
func (t *Topic) Exists(ctx context.Context) (bool, error) {
|
||||
if t.name == "_deleted-topic_" {
|
||||
return false, nil
|
||||
}
|
||||
_, err := t.c.pubc.GetTopic(ctx, &pb.GetTopicRequest{Topic: t.name})
|
||||
if err == nil {
|
||||
return true, nil
|
||||
}
|
||||
if grpc.Code(err) == codes.NotFound {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
func (t *Topic) IAM() *iam.Handle {
|
||||
return iam.InternalNewHandle(t.c.pubc.Connection(), t.name)
|
||||
}
|
||||
|
||||
// Subscriptions returns an iterator which returns the subscriptions for this topic.
|
||||
//
|
||||
// Some of the returned subscriptions may belong to a project other than t.
|
||||
func (t *Topic) Subscriptions(ctx context.Context) *SubscriptionIterator {
|
||||
it := t.c.pubc.ListTopicSubscriptions(ctx, &pb.ListTopicSubscriptionsRequest{
|
||||
Topic: t.name,
|
||||
})
|
||||
return &SubscriptionIterator{
|
||||
c: t.c,
|
||||
next: it.Next,
|
||||
}
|
||||
}
|
||||
|
||||
var errTopicStopped = errors.New("pubsub: Stop has been called for this topic")
|
||||
|
||||
// Publish publishes msg to the topic asynchronously. Messages are batched and
|
||||
// sent according to the topic's PublishSettings. Publish never blocks.
|
||||
//
|
||||
// Publish returns a non-nil PublishResult which will be ready when the
|
||||
// message has been sent (or has failed to be sent) to the server.
|
||||
//
|
||||
// Publish creates goroutines for batching and sending messages. These goroutines
|
||||
// need to be stopped by calling t.Stop(). Once stopped, future calls to Publish
|
||||
// will immediately return a PublishResult with an error.
|
||||
func (t *Topic) Publish(ctx context.Context, msg *Message) *PublishResult {
|
||||
// TODO(jba): if this turns out to take significant time, try to approximate it.
|
||||
// Or, convert the messages to protos in Publish, instead of in the service.
|
||||
msg.size = proto.Size(&pb.PubsubMessage{
|
||||
Data: msg.Data,
|
||||
Attributes: msg.Attributes,
|
||||
})
|
||||
r := &PublishResult{ready: make(chan struct{})}
|
||||
t.initBundler()
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
// TODO(aboulhosn) [from bcmills] consider changing the semantics of bundler to perform this logic so we don't have to do it here
|
||||
if t.stopped {
|
||||
r.set("", errTopicStopped)
|
||||
return r
|
||||
}
|
||||
|
||||
// TODO(jba) [from bcmills] consider using a shared channel per bundle
|
||||
// (requires Bundler API changes; would reduce allocations)
|
||||
// The call to Add should never return an error because the bundler's
|
||||
// BufferedByteLimit is set to maxInt; we do not perform any flow
|
||||
// control in the client.
|
||||
err := t.bundler.Add(&bundledMessage{msg, r}, msg.size)
|
||||
if err != nil {
|
||||
r.set("", err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// Send all remaining published messages and stop goroutines created for handling
|
||||
// publishing. Returns once all outstanding messages have been sent or have
|
||||
// failed to be sent.
|
||||
func (t *Topic) Stop() {
|
||||
t.mu.Lock()
|
||||
noop := t.stopped || t.bundler == nil
|
||||
t.stopped = true
|
||||
t.mu.Unlock()
|
||||
if noop {
|
||||
return
|
||||
}
|
||||
t.bundler.Flush()
|
||||
// At this point, all pending bundles have been published and the bundler's
|
||||
// goroutines have exited, so it is OK for this goroutine to close bundlec.
|
||||
close(t.bundlec)
|
||||
t.wg.Wait()
|
||||
}
|
||||
|
||||
// A PublishResult holds the result from a call to Publish.
|
||||
type PublishResult struct {
|
||||
ready chan struct{}
|
||||
serverID string
|
||||
err error
|
||||
}
|
||||
|
||||
// Ready returns a channel that is closed when the result is ready.
|
||||
// When the Ready channel is closed, Get is guaranteed not to block.
|
||||
func (r *PublishResult) Ready() <-chan struct{} { return r.ready }
|
||||
|
||||
// Get returns the server-generated message ID and/or error result of a Publish call.
|
||||
// Get blocks until the Publish call completes or the context is done.
|
||||
func (r *PublishResult) Get(ctx context.Context) (serverID string, err error) {
|
||||
// If the result is already ready, return it even if the context is done.
|
||||
select {
|
||||
case <-r.Ready():
|
||||
return r.serverID, r.err
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return "", ctx.Err()
|
||||
case <-r.Ready():
|
||||
return r.serverID, r.err
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PublishResult) set(sid string, err error) {
|
||||
r.serverID = sid
|
||||
r.err = err
|
||||
close(r.ready)
|
||||
}
|
||||
|
||||
type bundledMessage struct {
|
||||
msg *Message
|
||||
res *PublishResult
|
||||
}
|
||||
|
||||
func (t *Topic) initBundler() {
|
||||
t.mu.RLock()
|
||||
noop := t.stopped || t.bundler != nil
|
||||
t.mu.RUnlock()
|
||||
if noop {
|
||||
return
|
||||
}
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
// Must re-check, since we released the lock.
|
||||
if t.stopped || t.bundler != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(jba): use a context detached from the one passed to NewClient.
|
||||
ctx := context.TODO()
|
||||
// Unless overridden, run several goroutines per CPU to call the Publish RPC.
|
||||
n := t.PublishSettings.NumGoroutines
|
||||
if n <= 0 {
|
||||
n = 25 * runtime.GOMAXPROCS(0)
|
||||
}
|
||||
timeout := t.PublishSettings.Timeout
|
||||
t.wg.Add(n)
|
||||
for i := 0; i < n; i++ {
|
||||
go func() {
|
||||
defer t.wg.Done()
|
||||
for b := range t.bundlec {
|
||||
bctx := ctx
|
||||
cancel := func() {}
|
||||
if timeout != 0 {
|
||||
bctx, cancel = context.WithTimeout(ctx, timeout)
|
||||
}
|
||||
t.publishMessageBundle(bctx, b)
|
||||
cancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
t.bundler = bundler.NewBundler(&bundledMessage{}, func(items interface{}) {
|
||||
t.bundlec <- items.([]*bundledMessage)
|
||||
|
||||
})
|
||||
t.bundler.DelayThreshold = t.PublishSettings.DelayThreshold
|
||||
t.bundler.BundleCountThreshold = t.PublishSettings.CountThreshold
|
||||
if t.bundler.BundleCountThreshold > MaxPublishRequestCount {
|
||||
t.bundler.BundleCountThreshold = MaxPublishRequestCount
|
||||
}
|
||||
t.bundler.BundleByteThreshold = t.PublishSettings.ByteThreshold
|
||||
t.bundler.BufferedByteLimit = maxInt
|
||||
t.bundler.BundleByteLimit = MaxPublishRequestBytes
|
||||
}
|
||||
|
||||
func (t *Topic) publishMessageBundle(ctx context.Context, bms []*bundledMessage) {
|
||||
pbMsgs := make([]*pb.PubsubMessage, len(bms))
|
||||
for i, bm := range bms {
|
||||
pbMsgs[i] = &pb.PubsubMessage{
|
||||
Data: bm.msg.Data,
|
||||
Attributes: bm.msg.Attributes,
|
||||
}
|
||||
bm.msg = nil // release bm.msg for GC
|
||||
}
|
||||
res, err := t.c.pubc.Publish(ctx, &pb.PublishRequest{
|
||||
Topic: t.name,
|
||||
Messages: pbMsgs,
|
||||
}, gax.WithGRPCOptions(grpc.MaxCallSendMsgSize(maxSendRecvBytes)))
|
||||
for i, bm := range bms {
|
||||
if err != nil {
|
||||
bm.res.set("", err)
|
||||
} else {
|
||||
bm.res.set(res.MessageIds[i], nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
// Copyright 2016 Google Inc. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package pubsub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/internal/testutil"
|
||||
"google.golang.org/grpc/status"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
pubsubpb "google.golang.org/genproto/googleapis/pubsub/v1"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
func checkTopicListing(t *testing.T, c *Client, want []string) {
|
||||
topics, err := slurpTopics(c.Topics(context.Background()))
|
||||
if err != nil {
|
||||
t.Fatalf("error listing topics: %v", err)
|
||||
}
|
||||
var got []string
|
||||
for _, topic := range topics {
|
||||
got = append(got, topic.ID())
|
||||
}
|
||||
if !testutil.Equal(got, want) {
|
||||
t.Errorf("topic list: got: %v, want: %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// All returns the remaining topics from this iterator.
|
||||
func slurpTopics(it *TopicIterator) ([]*Topic, error) {
|
||||
var topics []*Topic
|
||||
for {
|
||||
switch topic, err := it.Next(); err {
|
||||
case nil:
|
||||
topics = append(topics, topic)
|
||||
case iterator.Done:
|
||||
return topics, nil
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopicID(t *testing.T) {
|
||||
const id = "id"
|
||||
c, _ := newFake(t)
|
||||
s := c.Topic(id)
|
||||
if got, want := s.ID(), id; got != want {
|
||||
t.Errorf("Token.ID() = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListTopics(t *testing.T) {
|
||||
c, _ := newFake(t)
|
||||
var ids []string
|
||||
for i := 1; i <= 4; i++ {
|
||||
id := fmt.Sprintf("t%d", i)
|
||||
ids = append(ids, id)
|
||||
mustCreateTopic(t, c, id)
|
||||
}
|
||||
checkTopicListing(t, c, ids)
|
||||
}
|
||||
|
||||
func TestListCompletelyEmptyTopics(t *testing.T) {
|
||||
c, _ := newFake(t)
|
||||
checkTopicListing(t, c, nil)
|
||||
}
|
||||
|
||||
func TestStopPublishOrder(t *testing.T) {
|
||||
// Check that Stop doesn't panic if called before Publish.
|
||||
// Also that Publish after Stop returns the right error.
|
||||
ctx := context.Background()
|
||||
c := &Client{projectID: "projid"}
|
||||
topic := c.Topic("t")
|
||||
topic.Stop()
|
||||
r := topic.Publish(ctx, &Message{})
|
||||
_, err := r.Get(ctx)
|
||||
if err != errTopicStopped {
|
||||
t.Errorf("got %v, want errTopicStopped", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishTimeout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
serv := grpc.NewServer()
|
||||
pubsubpb.RegisterPublisherServer(serv, &alwaysFailPublish{})
|
||||
lis, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
go serv.Serve(lis)
|
||||
conn, err := grpc.Dial(lis.Addr().String(), grpc.WithInsecure())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, err := NewClient(ctx, "projectID", option.WithGRPCConn(conn))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
topic := c.Topic("t")
|
||||
topic.PublishSettings.Timeout = 3 * time.Second
|
||||
r := topic.Publish(ctx, &Message{})
|
||||
defer topic.Stop()
|
||||
select {
|
||||
case <-r.Ready():
|
||||
_, err = r.Get(ctx)
|
||||
if err != context.DeadlineExceeded {
|
||||
t.Fatalf("got %v, want context.DeadlineExceeded", err)
|
||||
}
|
||||
case <-time.After(2 * topic.PublishSettings.Timeout):
|
||||
t.Fatal("timed out")
|
||||
}
|
||||
}
|
||||
|
||||
type alwaysFailPublish struct {
|
||||
pubsubpb.PublisherServer
|
||||
}
|
||||
|
||||
func (s *alwaysFailPublish) Publish(ctx context.Context, req *pubsubpb.PublishRequest) (*pubsubpb.PublishResponse, error) {
|
||||
return nil, status.Errorf(codes.Unavailable, "try again")
|
||||
}
|
||||
|
||||
func mustCreateTopic(t *testing.T, c *Client, id string) *Topic {
|
||||
topic, err := c.CreateTopic(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return topic
|
||||
}
|
||||
Reference in New Issue
Block a user