initial commit

Signed-off-by: Derek McQuay <derekmcquay@gmail.com>
This commit is contained in:
dm
2018-04-10 19:17:26 -07:00
commit 59af8fc84f
2025 changed files with 456739 additions and 0 deletions
+67
View File
@@ -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
View File
@@ -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
View File
@@ -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",
}
}
File diff suppressed because it is too large Load Diff
+95
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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)
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
@@ -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 })
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}