diff --git a/sdk/messaging/azservicebus/CHANGELOG.md b/sdk/messaging/azservicebus/CHANGELOG.md index a8affa04f884..d576cb5261a5 100644 --- a/sdk/messaging/azservicebus/CHANGELOG.md +++ b/sdk/messaging/azservicebus/CHANGELOG.md @@ -1,14 +1,24 @@ # Release History -## 0.2.1 (Unreleased) +## 0.3.0 (Unreleased) ### Features Added -### Breaking Changes +- AbandonMessage and DeferMessage now take an additional `PropertiesToModify` option, allowing + the message properties to be modified when they are settled. -### Bugs Fixed +### Breaking Changes -### Other Changes +- ReceivedMessage.Body is now a function that returns a ([]byte, error), rather than being a field. + This protects against a potential data-loss scenario where a message is received with a payload + encoded in the sequence or value sections of an AMQP message, which cannot be prpoerly represented + in the .Body. This will now return an error. +- Functions that have options or might have options in the future have an additional *options parameter. + As usual, passing 'nil' ignores the options, and will cause the function to use defaults. +- MessageBatch.Add() has been renamed to MessageBatch.AddMessage(). AddMessage() now returns only an `error`, + with a sentinel error (ErrMessageTooLarge) signaling that the batch cannot fit a new message. +- Sender.SendMessages() has been removed in favor of simplifications made in MessageBatch. +- AdminClient has been moved into the `admin` subpackage. ## 0.2.0 (2021-11-02) diff --git a/sdk/messaging/azservicebus/README.md b/sdk/messaging/azservicebus/README.md index 88d0e40f3710..a2e3afc6c58c 100644 --- a/sdk/messaging/azservicebus/README.md +++ b/sdk/messaging/azservicebus/README.md @@ -46,7 +46,7 @@ func main() { client, err := azservicebus.NewClientFromConnectionString("") if err != nil { - log.Fatalf("Failed to create Service Bus Client: %s", err.Error()) + panic(err) } } ``` @@ -66,13 +66,13 @@ func main() { cred, err := azidentity.NewDefaultAzureCredential(nil) if err != nil { - log.Fatalf("Failed creating DefaultAzureCredential: %s", err.Error()) + panic(err) } client, err := azservicebus.NewClient("", cred) if err != nil { - log.Fatalf("Failed to create Service Bus Client: %s", err.Error()) + panic(err) } } ``` @@ -112,7 +112,7 @@ NOTE: Creating a `client` is covered in the ["Authenticate the client"](#authent sender, err := client.NewSender("") if err != nil { - log.Fatalf("Failed to create Sender: %s", err.Error()) + panic(err) } // send a single message @@ -129,29 +129,23 @@ You can also send messages in batches, which can be more efficient than sending messageBatch, err := sender.NewMessageBatch(context.TODO()) if err != nil { - log.Fatalf("Failed to create a message batch: %s", err.Error()) + panic(err) } -// Add a message using TryAdd. -// This can be called multiple times, and will return (false, nil) -// if the message cannot be added because the batch is full. -added, err := messageBatch.TryAdd(&azservicebus.Message{ +// Add a message to our message batch. This can be called multiple times. +err := messageBatch.Add(&azservicebus.Message{ Body: []byte(fmt.Sprintf("hello world")), }) -if err != nil { - log.Fatalf("Failed to add message to batch because of an error: %s", err.Error()) -} +if err == azservicebus.ErrMessageTooLarge { + fmt.Printf("Message batch is full. We should send it and create a new one.\n") -if !added { - log.Printf("Message batch is full. We should send it and create a new one.") + // send what we have since the batch is full err := sender.SendMessageBatch(context.TODO(), messageBatch) - - if err != nil { - log.Fatalf("Failed to send message batch: %s", err.Error()) - } - - // add the next message to a new batch and start again. + + // Create a new batch, add this message and start again. +} else if err != nil { + panic(err) } ``` @@ -172,7 +166,7 @@ receiver, err := client.NewReceiverForQueue( // client.NewReceiverForSubscription("", "") if err != nil { - log.Fatalf("Failed to create the receiver: %s", err.Error()) + panic(err) } // Receive a fixed set of messages. Note that the number of messages diff --git a/sdk/messaging/azservicebus/admin/admin_client.go b/sdk/messaging/azservicebus/admin/admin_client.go new file mode 100644 index 000000000000..2755b72ebfcf --- /dev/null +++ b/sdk/messaging/azservicebus/admin/admin_client.go @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package admin + +import ( + "context" + "fmt" + "net/http" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/atom" +) + +// Client allows you to administer resources in a Service Bus Namespace. +// For example, you can create queues, enabling capabilities like partitioning, duplicate detection, etc.. +// NOTE: For sending and receiving messages you'll need to use the `azservicebus.Client` type instead. +type Client struct { + em atom.EntityManager +} + +type ClientOptions struct { + // for future expansion +} + +// NewClientFromConnectionString creates a Client authenticating using a connection string. +func NewClientFromConnectionString(connectionString string, options *ClientOptions) (*Client, error) { + em, err := atom.NewEntityManagerWithConnectionString(connectionString, internal.Version) + + if err != nil { + return nil, err + } + + return &Client{em: em}, nil +} + +// NewClient creates a Client authenticating using a TokenCredential. +func NewClient(fullyQualifiedNamespace string, tokenCredential azcore.TokenCredential, options *ClientOptions) (*Client, error) { + em, err := atom.NewEntityManager(fullyQualifiedNamespace, tokenCredential, internal.Version) + + if err != nil { + return nil, err + } + + return &Client{em: em}, nil +} + +type NamespaceProperties struct { + CreatedTime time.Time + ModifiedTime time.Time + + SKU string + MessagingUnits *int64 + Name string +} + +type GetNamespacePropertiesResult struct { + NamespaceProperties +} + +type GetNamespacePropertiesResponse struct { + GetNamespacePropertiesResult + RawResponse *http.Response +} + +type GetNamespacePropertiesOptions struct { + // For future expansion +} + +// GetNamespaceProperties gets the properties for the namespace, includings properties like SKU and CreatedTime. +func (ac *Client) GetNamespaceProperties(ctx context.Context, options *GetNamespacePropertiesOptions) (*GetNamespacePropertiesResponse, error) { + var body *atom.NamespaceEntry + resp, err := ac.em.Get(ctx, "/$namespaceinfo", &body) + + if err != nil { + return nil, err + } + + return &GetNamespacePropertiesResponse{ + RawResponse: resp, + GetNamespacePropertiesResult: GetNamespacePropertiesResult{ + NamespaceProperties: NamespaceProperties{ + Name: body.NamespaceInfo.Name, + CreatedTime: body.NamespaceInfo.CreatedTime, + ModifiedTime: body.NamespaceInfo.ModifiedTime, + SKU: body.NamespaceInfo.MessagingSKU, + MessagingUnits: body.NamespaceInfo.MessagingUnits, + }, + }, + }, nil +} + +type pagerFunc func(ctx context.Context, pv interface{}) (*http.Response, error) + +// newPagerFunc gets a function that can be used to page sequentially through an ATOM resource +func (ac *Client) newPagerFunc(baseFragment string, maxPageSize int32, lenV func(pv interface{}) int) pagerFunc { + eof := false + skip := int32(0) + + return func(ctx context.Context, pv interface{}) (*http.Response, error) { + if eof { + return nil, nil + } + + url := baseFragment + "?" + if maxPageSize > 0 { + url += fmt.Sprintf("&$top=%d", maxPageSize) + } + + if skip > 0 { + url += fmt.Sprintf("&$skip=%d", skip) + } + + resp, err := ac.em.Get(ctx, url, pv) + + if err != nil { + eof = true + return nil, err + } + + if lenV(pv) == 0 { + eof = true + return nil, nil + } + + if lenV(pv) < int(maxPageSize) { + eof = true + } + + skip += maxPageSize + return resp, nil + } +} diff --git a/sdk/messaging/azservicebus/admin/admin_client_models.go b/sdk/messaging/azservicebus/admin/admin_client_models.go new file mode 100644 index 000000000000..f1fc7be7bc30 --- /dev/null +++ b/sdk/messaging/azservicebus/admin/admin_client_models.go @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package admin + +// EntityStatus represents the current status of the entity. +type EntityStatus string + +const ( + // EntityStatusActive indicates an entity can be used for sending and receiving. + EntityStatusActive EntityStatus = "Active" + // EntityStatusDisabled indicates an entity cannot be used for sending or receiving. + EntityStatusDisabled EntityStatus = "Disabled" + // EntityStatusSendDisabled indicates that an entity cannot be used for sending. + EntityStatusSendDisabled EntityStatus = "SendDisabled" + // EntityStatusReceiveDisabled indicates that an entity cannot be used for receiving. + EntityStatusReceiveDisabled EntityStatus = "ReceiveDisabled" +) diff --git a/sdk/messaging/azservicebus/admin_client_queue.go b/sdk/messaging/azservicebus/admin/admin_client_queue.go similarity index 72% rename from sdk/messaging/azservicebus/admin_client_queue.go rename to sdk/messaging/azservicebus/admin/admin_client_queue.go index 4c90df89fb07..a3dce82d72ca 100644 --- a/sdk/messaging/azservicebus/admin_client_queue.go +++ b/sdk/messaging/azservicebus/admin/admin_client_queue.go @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package azservicebus +package admin import ( "context" - "errors" - "fmt" "net/http" "time" @@ -18,9 +16,6 @@ import ( // QueueProperties represents the static properties of the queue. type QueueProperties struct { - // Name of the queue relative to the namespace base address. - Name string - // LockDuration is the duration a message is locked when using the PeekLock receive mode. // Default is 1 minute. LockDuration *time.Duration @@ -74,13 +69,13 @@ type QueueProperties struct { // ForwardDeadLetteredMessagesTo is the absolute URI of the entity to forward dead letter messages ForwardDeadLetteredMessagesTo *string + + // UserMetadata is custom metadata that user can associate with the queue. + UserMetadata *string } // QueueRuntimeProperties represent dynamic properties of a queue, such as the ActiveMessageCount. type QueueRuntimeProperties struct { - // Name is the name of the queue. - Name string - // SizeInBytes - The size of the queue, in bytes. SizeInBytes int64 @@ -113,64 +108,81 @@ type QueueRuntimeProperties struct { TransferMessageCount int32 } -type AddQueueResponse struct { - // Value is the result of the request. - Value *QueueProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type CreateQueueOptions struct { + // for future expansion } -type AddQueueWithPropertiesResponse struct { - // Value is the result of the request. - Value *QueueProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type CreateQueueResult struct { + QueueProperties } -type GetQueueResponse struct { - // Value is the result of the request. - Value *QueueProperties +type CreateQueueResponse struct { + CreateQueueResult + // RawResponse is the *http.Response for the request. RawResponse *http.Response } -type UpdateQueueResponse struct { - // Value is the result of the request. - Value *QueueProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +// CreateQueue creates a queue with configurable properties. +func (ac *Client) CreateQueue(ctx context.Context, queueName string, properties *QueueProperties, options *CreateQueueOptions) (*CreateQueueResponse, error) { + newProps, resp, err := ac.createOrUpdateQueueImpl(ctx, queueName, properties, true) + + if err != nil { + return nil, err + } + + return &CreateQueueResponse{ + RawResponse: resp, + CreateQueueResult: CreateQueueResult{ + QueueProperties: *newProps, + }, + }, nil } -type GetQueueRuntimePropertiesResponse struct { - // Value is the result of the request. - Value *QueueRuntimeProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type UpdateQueueResult struct { + QueueProperties } -type DeleteQueueResponse struct { +type UpdateQueueResponse struct { + UpdateQueueResult + + // RawResponse is the *http.Response for the request. RawResponse *http.Response } -// AddQueue creates a queue using defaults for all options. -func (ac *AdminClient) AddQueue(ctx context.Context, queueName string) (*AddQueueResponse, error) { - return ac.AddQueueWithProperties(ctx, &QueueProperties{ - Name: queueName, - }) +type UpdateQueueOptions struct { + // for future expansion } -// CreateQueue creates a queue with configurable properties. -func (ac *AdminClient) AddQueueWithProperties(ctx context.Context, properties *QueueProperties) (*AddQueueResponse, error) { - props, resp, err := ac.createOrUpdateQueueImpl(ctx, properties, true) +// UpdateQueue updates an existing queue. +func (ac *Client) UpdateQueue(ctx context.Context, queueName string, properties QueueProperties, options *UpdateQueueOptions) (*UpdateQueueResponse, error) { + newProps, resp, err := ac.createOrUpdateQueueImpl(ctx, queueName, &properties, false) + + if err != nil { + return nil, err + } - return &AddQueueResponse{ + return &UpdateQueueResponse{ RawResponse: resp, - Value: props, + UpdateQueueResult: UpdateQueueResult{ + QueueProperties: *newProps, + }, }, err } +type GetQueueResult struct { + QueueProperties +} + +type GetQueueResponse struct { + GetQueueResult + + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + // GetQueue gets a queue by name. -func (ac *AdminClient) GetQueue(ctx context.Context, queueName string) (*GetQueueResponse, error) { +func (ac *Client) GetQueue(ctx context.Context, queueName string) (*GetQueueResponse, error) { var atomResp *atom.QueueEnvelope resp, err := ac.em.Get(ctx, "/"+queueName, &atomResp) @@ -178,7 +190,7 @@ func (ac *AdminClient) GetQueue(ctx context.Context, queueName string) (*GetQueu return nil, err } - props, err := newQueueProperties(atomResp.Title, &atomResp.Content.QueueDescription) + props, err := newQueueProperties(&atomResp.Content.QueueDescription) if err != nil { return nil, err @@ -186,12 +198,25 @@ func (ac *AdminClient) GetQueue(ctx context.Context, queueName string) (*GetQueu return &GetQueueResponse{ RawResponse: resp, - Value: props, + GetQueueResult: GetQueueResult{ + QueueProperties: *props, + }, }, nil } +type GetQueueRuntimePropertiesResult struct { + QueueRuntimeProperties +} + +type GetQueueRuntimePropertiesResponse struct { + GetQueueRuntimePropertiesResult + + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + // GetQueueRuntimeProperties gets runtime properties of a queue, like the SizeInBytes, or ActiveMessageCount. -func (ac *AdminClient) GetQueueRuntimeProperties(ctx context.Context, queueName string) (*GetQueueRuntimePropertiesResponse, error) { +func (ac *Client) GetQueueRuntimeProperties(ctx context.Context, queueName string) (*GetQueueRuntimePropertiesResponse, error) { var atomResp *atom.QueueEnvelope resp, err := ac.em.Get(ctx, "/"+queueName, &atomResp) @@ -199,7 +224,7 @@ func (ac *AdminClient) GetQueueRuntimeProperties(ctx context.Context, queueName return nil, err } - props, err := newQueueRuntimeProperties(atomResp.Title, &atomResp.Content.QueueDescription), nil + props, err := newQueueRuntimeProperties(&atomResp.Content.QueueDescription), nil if err != nil { return nil, err @@ -207,44 +232,22 @@ func (ac *AdminClient) GetQueueRuntimeProperties(ctx context.Context, queueName return &GetQueueRuntimePropertiesResponse{ RawResponse: resp, - Value: props, + GetQueueRuntimePropertiesResult: GetQueueRuntimePropertiesResult{ + QueueRuntimeProperties: *props, + }, }, nil } -// QueueExists checks if a queue exists. -// Returns true if the queue is found -// (false, nil) if the queue is not found -// (false, err) if an error occurred while trying to check if the queue exists. -func (ac *AdminClient) QueueExists(ctx context.Context, queueName string) (bool, error) { - _, err := ac.GetQueue(ctx, queueName) - - if err == nil { - return true, nil - } - - if atom.NotFound(err) { - return false, nil - } - - return false, err +type DeleteQueueResponse struct { + RawResponse *http.Response } -// UpdateQueue updates an existing queue. -func (ac *AdminClient) UpdateQueue(ctx context.Context, properties *QueueProperties) (*UpdateQueueResponse, error) { - newProps, resp, err := ac.createOrUpdateQueueImpl(ctx, properties, false) - - if err != nil { - return nil, err - } - - return &UpdateQueueResponse{ - RawResponse: resp, - Value: newProps, - }, err +type DeleteQueueOptions struct { + // for future expansion } // DeleteQueue deletes a queue. -func (ac *AdminClient) DeleteQueue(ctx context.Context, queueName string) (*DeleteQueueResponse, error) { +func (ac *Client) DeleteQueue(ctx context.Context, queueName string, options *DeleteQueueOptions) (*DeleteQueueResponse, error) { resp, err := ac.em.Delete(ctx, "/"+queueName) defer atom.CloseRes(ctx, resp) return &DeleteQueueResponse{RawResponse: resp}, err @@ -252,14 +255,12 @@ func (ac *AdminClient) DeleteQueue(ctx context.Context, queueName string) (*Dele // ListQueuesOptions can be used to configure the ListQueues method. type ListQueuesOptions struct { - // Top is the maximum size of each page of results. - Top int - // Skip is the starting index for the paging operation. - Skip int + // MaxPageSize is the maximum size of each page of results. + MaxPageSize int32 } -// QueuePropertiesPager provides iteration over ListQueueProperties pages. -type QueuePropertiesPager interface { +// QueueItemPager provides iteration over ListQueueProperties pages. +type QueueItemPager interface { // NextPage returns true if the pager advanced to the next page. // Returns false if there are no more pages or an error occurred. NextPage(context.Context) bool @@ -271,39 +272,54 @@ type QueuePropertiesPager interface { Err() error } +type ListQueuesResult struct { + Items []*QueueItem +} + type ListQueuesResponse struct { + ListQueuesResult RawResponse *http.Response - Value []*QueueProperties +} + +type QueueItem struct { + QueueName string + QueueProperties } // ListQueues lists queues. -func (ac *AdminClient) ListQueues(options *ListQueuesOptions) QueuePropertiesPager { - var pageSize int - var skip int +func (ac *Client) ListQueues(options *ListQueuesOptions) QueueItemPager { + var pageSize int32 if options != nil { - skip = options.Skip - pageSize = options.Top + pageSize = options.MaxPageSize } return &queuePropertiesPager{ - innerPager: ac.getQueuePager(pageSize, skip), + innerPager: ac.newPagerFunc("/$Resources/Queues", pageSize, queueFeedLen), } } +func queueFeedLen(v interface{}) int { + feed := v.(**atom.QueueFeed) + return len((*feed).Entries) +} + // ListQueuesRuntimePropertiesOptions can be used to configure the ListQueuesRuntimeProperties method. type ListQueuesRuntimePropertiesOptions struct { - // Top is the maximum size of each page of results. - Top int - // Skip is the starting index for the paging operation. - Skip int + // MaxPageSize is the maximum size of each page of results. + MaxPageSize int32 } type ListQueuesRuntimePropertiesResponse struct { - Value []*QueueRuntimeProperties + Items []*QueueRuntimePropertiesItem RawResponse *http.Response } +type QueueRuntimePropertiesItem struct { + QueueName string + QueueRuntimeProperties +} + // QueueRuntimePropertiesPager provides iteration over ListQueueRuntimeProperties pages. type QueueRuntimePropertiesPager interface { // NextPage returns true if the pager advanced to the next page. @@ -318,23 +334,21 @@ type QueueRuntimePropertiesPager interface { } // ListQueuesRuntimeProperties lists runtime properties for queues. -func (ac *AdminClient) ListQueuesRuntimeProperties(options *ListQueuesRuntimePropertiesOptions) QueueRuntimePropertiesPager { - var pageSize int - var skip int +func (ac *Client) ListQueuesRuntimeProperties(options *ListQueuesRuntimePropertiesOptions) QueueRuntimePropertiesPager { + var pageSize int32 if options != nil { - skip = options.Skip - pageSize = options.Top + pageSize = options.MaxPageSize } return &queueRuntimePropertiesPager{ - innerPager: ac.getQueuePager(pageSize, skip), + innerPager: ac.newPagerFunc("/$Resources/Queues", pageSize, queueFeedLen), } } -func (ac *AdminClient) createOrUpdateQueueImpl(ctx context.Context, props *QueueProperties, creating bool) (*QueueProperties, *http.Response, error) { +func (ac *Client) createOrUpdateQueueImpl(ctx context.Context, queueName string, props *QueueProperties, creating bool) (*QueueProperties, *http.Response, error) { if props == nil { - return nil, nil, errors.New("properties are required and cannot be nil") + props = &QueueProperties{} } env, mw := newQueueEnvelope(props, ac.em.TokenProvider()) @@ -350,13 +364,13 @@ func (ac *AdminClient) createOrUpdateQueueImpl(ctx context.Context, props *Queue } var atomResp *atom.QueueEnvelope - resp, err := ac.em.Put(ctx, "/"+props.Name, env, &atomResp, mw...) + resp, err := ac.em.Put(ctx, "/"+queueName, env, &atomResp, mw...) if err != nil { return nil, nil, err } - newProps, err := newQueueProperties(props.Name, &atomResp.Content.QueueDescription) + newProps, err := newQueueProperties(&atomResp.Content.QueueDescription) if err != nil { return nil, nil, err @@ -367,7 +381,7 @@ func (ac *AdminClient) createOrUpdateQueueImpl(ctx context.Context, props *Queue // queuePropertiesPager provides iteration over QueueProperties pages. type queuePropertiesPager struct { - innerPager queueFeedPagerFunc + innerPager pagerFunc lastErr error lastResponse *ListQueuesResponse @@ -391,37 +405,40 @@ func (p *queuePropertiesPager) Err() error { } func (p *queuePropertiesPager) getNextPage(ctx context.Context) (*ListQueuesResponse, error) { - feed, resp, err := p.innerPager(ctx) + var feed *atom.QueueFeed + resp, err := p.innerPager(ctx, &feed) - if err != nil { + if err != nil || feed == nil { return nil, err } - if feed == nil { - return nil, nil - } - - var all []*QueueProperties + var all []*QueueItem for _, env := range feed.Entries { - props, err := newQueueProperties(env.Title, &env.Content.QueueDescription) + queueName := env.Title + props, err := newQueueProperties(&env.Content.QueueDescription) if err != nil { return nil, err } - all = append(all, props) + all = append(all, &QueueItem{ + QueueName: queueName, + QueueProperties: *props, + }) } return &ListQueuesResponse{ RawResponse: resp, - Value: all, + ListQueuesResult: ListQueuesResult{ + Items: all, + }, }, nil } // queueRuntimePropertiesPager provides iteration over QueueRuntimeProperties pages. type queueRuntimePropertiesPager struct { - innerPager queueFeedPagerFunc + innerPager pagerFunc lastErr error lastResponse *ListQueuesRuntimePropertiesResponse } @@ -434,21 +451,25 @@ func (p *queueRuntimePropertiesPager) NextPage(ctx context.Context) bool { } func (p *queueRuntimePropertiesPager) getNextPage(ctx context.Context) (*ListQueuesRuntimePropertiesResponse, error) { - feed, resp, err := p.innerPager(ctx) + var feed *atom.QueueFeed + resp, err := p.innerPager(ctx, &feed) if err != nil || feed == nil { return nil, err } - var all []*QueueRuntimeProperties + var all []*QueueRuntimePropertiesItem for _, entry := range feed.Entries { - all = append(all, newQueueRuntimeProperties(entry.Title, &entry.Content.QueueDescription)) + all = append(all, &QueueRuntimePropertiesItem{ + QueueName: entry.Title, + QueueRuntimeProperties: *newQueueRuntimeProperties(&entry.Content.QueueDescription), + }) } return &ListQueuesRuntimePropertiesResponse{ RawResponse: resp, - Value: all, + Items: all, }, nil } @@ -462,35 +483,6 @@ func (p *queueRuntimePropertiesPager) Err() error { return p.lastErr } -type queueFeedPagerFunc func(ctx context.Context) (*atom.QueueFeed, *http.Response, error) - -func (ac *AdminClient) getQueuePager(top int, skip int) queueFeedPagerFunc { - return func(ctx context.Context) (*atom.QueueFeed, *http.Response, error) { - url := "/$Resources/Queues?" - if top > 0 { - url += fmt.Sprintf("&$top=%d", top) - } - - if skip > 0 { - url += fmt.Sprintf("&$skip=%d", skip) - } - - var atomResp *atom.QueueFeed - resp, err := ac.em.Get(ctx, url, &atomResp) - - if err != nil { - return nil, nil, err - } - - if len(atomResp.Entries) == 0 { - return nil, nil, nil - } - - skip += len(atomResp.Entries) - return atomResp, resp, nil - } -} - func newQueueEnvelope(props *QueueProperties, tokenProvider auth.TokenProvider) (*atom.QueueEnvelope, []atom.MiddlewareFunc) { qpr := &atom.QueueDescription{ LockDuration: utils.DurationToStringPtr(props.LockDuration), @@ -507,12 +499,13 @@ func newQueueEnvelope(props *QueueProperties, tokenProvider auth.TokenProvider) EnablePartitioning: props.EnablePartitioning, ForwardTo: props.ForwardTo, ForwardDeadLetteredMessagesTo: props.ForwardDeadLetteredMessagesTo, + UserMetadata: props.UserMetadata, } return atom.WrapWithQueueEnvelope(qpr, tokenProvider) } -func newQueueProperties(name string, desc *atom.QueueDescription) (*QueueProperties, error) { +func newQueueProperties(desc *atom.QueueDescription) (*QueueProperties, error) { lockDuration, err := utils.ISO8601StringToDuration(desc.LockDuration) if err != nil { @@ -538,7 +531,6 @@ func newQueueProperties(name string, desc *atom.QueueDescription) (*QueuePropert } queuePropsResult := &QueueProperties{ - Name: name, LockDuration: lockDuration, MaxSizeInMegabytes: desc.MaxSizeInMegabytes, RequiresDuplicateDetection: desc.RequiresDuplicateDetection, @@ -553,14 +545,14 @@ func newQueueProperties(name string, desc *atom.QueueDescription) (*QueuePropert EnablePartitioning: desc.EnablePartitioning, ForwardTo: desc.ForwardTo, ForwardDeadLetteredMessagesTo: desc.ForwardDeadLetteredMessagesTo, + UserMetadata: desc.UserMetadata, } return queuePropsResult, nil } -func newQueueRuntimeProperties(name string, desc *atom.QueueDescription) *QueueRuntimeProperties { +func newQueueRuntimeProperties(desc *atom.QueueDescription) *QueueRuntimeProperties { return &QueueRuntimeProperties{ - Name: name, SizeInBytes: int64OrZero(desc.SizeInBytes), CreatedAt: dateTimeToTime(desc.CreatedAt), UpdatedAt: dateTimeToTime(desc.UpdatedAt), diff --git a/sdk/messaging/azservicebus/admin_client_subscription.go b/sdk/messaging/azservicebus/admin/admin_client_subscription.go similarity index 66% rename from sdk/messaging/azservicebus/admin_client_subscription.go rename to sdk/messaging/azservicebus/admin/admin_client_subscription.go index 0175efb93d3f..5f7299fa9466 100644 --- a/sdk/messaging/azservicebus/admin_client_subscription.go +++ b/sdk/messaging/azservicebus/admin/admin_client_subscription.go @@ -1,11 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package azservicebus +package admin import ( "context" - "errors" "fmt" "net/http" "time" @@ -17,9 +16,6 @@ import ( // SubscriptionProperties represents the static properties of the subscription. type SubscriptionProperties struct { - // Name of the subscription relative to the namespace base address. - Name string - // LockDuration is the duration a message is locked when using the PeekLock receive mode. // Default is 1 minute. LockDuration *time.Duration @@ -50,6 +46,9 @@ type SubscriptionProperties struct { // Status is the current status of the queue. Status *EntityStatus + // AutoDeleteOnIdle is the idle interval after which the subscription is automatically deleted. + AutoDeleteOnIdle *time.Duration + // ForwardTo is the name of the recipient entity to which all the messages sent to the queue // are forwarded to. ForwardTo *string @@ -60,15 +59,12 @@ type SubscriptionProperties struct { // EnableBatchedOperations indicates whether server-side batched operations are enabled. EnableBatchedOperations *bool - // UserMetadata is custom metadata that user can associate with the description. + // UserMetadata is custom metadata that user can associate with the subscription. UserMetadata *string } // SubscriptionRuntimeProperties represent dynamic properties of a subscription, such as the ActiveMessageCount. type SubscriptionRuntimeProperties struct { - // Name is the name of the subscription. - Name string - // TotalMessageCount is the number of messages in the subscription. TotalMessageCount int64 @@ -95,76 +91,54 @@ type SubscriptionRuntimeProperties struct { UpdatedAt time.Time } -type AddSubscriptionResponse struct { - // Value is the result of the request. - Value *SubscriptionProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type CreateSubscriptionResult struct { + SubscriptionProperties } -type GetSubscriptionResponse struct { +type CreateSubscriptionResponse struct { // Value is the result of the request. - Value *SubscriptionProperties + CreateSubscriptionResult // RawResponse is the *http.Response for the request. RawResponse *http.Response } -type GetSubscriptionRuntimePropertiesResponse struct { - // Value is the result of the request. - Value *SubscriptionRuntimeProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type CreateSubscriptionOptions struct { + // For future expansion } -type ListSubscriptionsResponse struct { - // Value is the result of the request. - Value []*SubscriptionProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response -} +// CreateSubscription creates a subscription to a topic with configurable properties +func (ac *Client) CreateSubscription(ctx context.Context, topicName string, subscriptionName string, properties *SubscriptionProperties, options *CreateSubscriptionOptions) (*CreateSubscriptionResponse, error) { + newProps, resp, err := ac.createOrUpdateSubscriptionImpl(ctx, topicName, subscriptionName, properties, true) -type ListSubscriptionsRuntimePropertiesResponse struct { - // Value is the result of the request. - Value []*SubscriptionRuntimeProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response + if err != nil { + return nil, err + } + + return &CreateSubscriptionResponse{ + RawResponse: resp, + CreateSubscriptionResult: CreateSubscriptionResult{ + SubscriptionProperties: *newProps, + }, + }, nil } -type UpdateSubscriptionResponse struct { - // Value is the result of the request. - Value *SubscriptionProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type GetSubscriptionResult struct { + SubscriptionProperties } -type DeleteSubscriptionResponse struct { +type GetSubscriptionResponse struct { + GetSubscriptionResult + // RawResponse is the *http.Response for the request. RawResponse *http.Response } -// AddSubscription creates a subscription to a topic using defaults for all options. -func (ac *AdminClient) AddSubscription(ctx context.Context, topicName string, subscriptionName string) (*AddSubscriptionResponse, error) { - return ac.AddSubscriptionWithProperties(ctx, topicName, &SubscriptionProperties{ - Name: subscriptionName, - }) -} - -// AddSubscriptionWithProperties creates a subscription to a topic with configurable properties -func (ac *AdminClient) AddSubscriptionWithProperties(ctx context.Context, topicName string, properties *SubscriptionProperties) (*AddSubscriptionResponse, error) { - newProps, resp, err := ac.createOrUpdateSubscriptionImpl(ctx, topicName, properties, true) - - if err != nil { - return nil, err - } - - return &AddSubscriptionResponse{ - RawResponse: resp, - Value: newProps, - }, nil +type GetSubscriptionOptions struct { + // For future expansion } // GetSubscription gets a subscription by name. -func (ac *AdminClient) GetSubscription(ctx context.Context, topicName string, subscriptionName string) (*GetSubscriptionResponse, error) { +func (ac *Client) GetSubscription(ctx context.Context, topicName string, subscriptionName string, options *GetSubscriptionOptions) (*GetSubscriptionResponse, error) { var atomResp *atom.SubscriptionEnvelope resp, err := ac.em.Get(ctx, fmt.Sprintf("/%s/Subscriptions/%s", topicName, subscriptionName), &atomResp) @@ -172,7 +146,7 @@ func (ac *AdminClient) GetSubscription(ctx context.Context, topicName string, su return nil, err } - props, err := newSubscriptionProperties(topicName, atomResp.Title, &atomResp.Content.SubscriptionDescription) + props, err := newSubscriptionProperties(&atomResp.Content.SubscriptionDescription) if err != nil { return nil, err @@ -180,12 +154,24 @@ func (ac *AdminClient) GetSubscription(ctx context.Context, topicName string, su return &GetSubscriptionResponse{ RawResponse: resp, - Value: props, + GetSubscriptionResult: GetSubscriptionResult{ + SubscriptionProperties: *props, + }, }, nil } +type GetSubscriptionRuntimePropertiesResult struct { + SubscriptionRuntimeProperties +} + +type GetSubscriptionRuntimePropertiesResponse struct { + GetSubscriptionRuntimePropertiesResult + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + // GetSubscriptionRuntimeProperties gets runtime properties of a subscription, like the SizeInBytes, or SubscriptionCount. -func (ac *AdminClient) GetSubscriptionRuntimeProperties(ctx context.Context, topicName string, subscriptionName string) (*GetSubscriptionRuntimePropertiesResponse, error) { +func (ac *Client) GetSubscriptionRuntimeProperties(ctx context.Context, topicName string, subscriptionName string) (*GetSubscriptionRuntimePropertiesResponse, error) { var atomResp *atom.SubscriptionEnvelope rawResp, err := ac.em.Get(ctx, fmt.Sprintf("/%s/Subscriptions/%s", topicName, subscriptionName), &atomResp) @@ -195,16 +181,16 @@ func (ac *AdminClient) GetSubscriptionRuntimeProperties(ctx context.Context, top return &GetSubscriptionRuntimePropertiesResponse{ RawResponse: rawResp, - Value: newSubscriptionRuntimeProperties(topicName, atomResp.Title, &atomResp.Content.SubscriptionDescription), + GetSubscriptionRuntimePropertiesResult: GetSubscriptionRuntimePropertiesResult{ + SubscriptionRuntimeProperties: *newSubscriptionRuntimeProperties(&atomResp.Content.SubscriptionDescription), + }, }, nil } // ListSubscriptionsOptions can be used to configure the ListSusbscriptions method. type ListSubscriptionsOptions struct { - // Top is the maximum size of each page of results. - Top int - // Skip is the starting index for the paging operation. - Skip int + // MaxPageSize is the maximum size of each page of results. + MaxPageSize int32 } // SubscriptionPropertiesPager provides iteration over ListSubscriptionProperties pages. @@ -220,28 +206,57 @@ type SubscriptionPropertiesPager interface { Err() error } +type SubscriptionPropertiesItem struct { + SubscriptionProperties + + TopicName string + SubscriptionName string +} + +type ListSubscriptionsResponse struct { + // Value is the result of the request. + Items []*SubscriptionPropertiesItem + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + // ListSubscriptions lists subscriptions for a topic. -func (ac *AdminClient) ListSubscriptions(topicName string, options *ListSubscriptionsOptions) SubscriptionPropertiesPager { - var pageSize int - var skip int +func (ac *Client) ListSubscriptions(topicName string, options *ListSubscriptionsOptions) SubscriptionPropertiesPager { + var pageSize int32 if options != nil { - skip = options.Skip - pageSize = options.Top + pageSize = options.MaxPageSize } return &subscriptionPropertiesPager{ topicName: topicName, - innerPager: ac.getSubscriptionPager(topicName, pageSize, skip), + innerPager: ac.newPagerFunc(fmt.Sprintf("/%s/Subscriptions?", topicName), pageSize, subFeedLen), } } +func subFeedLen(v interface{}) int { + feed := v.(**atom.SubscriptionFeed) + return len((*feed).Entries) +} + // ListSubscriptionsRuntimePropertiesOptions can be used to configure the ListSubscriptionsRuntimeProperties method. type ListSubscriptionsRuntimePropertiesOptions struct { - // Top is the maximum size of each page of results. - Top int - // Skip is the starting index for the paging operation. - Skip int + // MaxPageSize is the maximum size of each page of results. + MaxPageSize int32 +} + +type SubscriptionRuntimePropertiesItem struct { + SubscriptionRuntimeProperties + + TopicName string + SubscriptionName string +} + +type ListSubscriptionsRuntimePropertiesResponse struct { + // Value is the result of the request. + Items []*SubscriptionRuntimePropertiesItem + // RawResponse is the *http.Response for the request. + RawResponse *http.Response } // SubscriptionRuntimePropertiesPager provides iteration over ListTopicRuntimeProperties pages. @@ -258,41 +273,36 @@ type SubscriptionRuntimePropertiesPager interface { } // ListSubscriptionsRuntimeProperties lists runtime properties for subscriptions for a topic. -func (ac *AdminClient) ListSubscriptionsRuntimeProperties(topicName string, options *ListSubscriptionsRuntimePropertiesOptions) SubscriptionRuntimePropertiesPager { - var pageSize int - var skip int +func (ac *Client) ListSubscriptionsRuntimeProperties(topicName string, options *ListSubscriptionsRuntimePropertiesOptions) SubscriptionRuntimePropertiesPager { + var pageSize int32 if options != nil { - skip = options.Skip - pageSize = options.Top + pageSize = options.MaxPageSize } return &subscriptionRuntimePropertiesPager{ - innerPager: ac.getSubscriptionPager(topicName, pageSize, skip), + innerPager: ac.newPagerFunc(fmt.Sprintf("/%s/Subscriptions?", topicName), pageSize, subFeedLen), } } -// SubscriptionExists checks if a subscription exists. -// Returns true if the subscription is found -// (false, nil) if the subscription is not found -// (false, err) if an error occurred while trying to check if the subscription exists. -func (ac *AdminClient) SubscriptionExists(ctx context.Context, topicName string, subscriptionName string) (bool, error) { - _, err := ac.GetSubscription(ctx, topicName, subscriptionName) +type UpdateSubscriptionResult struct { + SubscriptionProperties +} - if err == nil { - return true, nil - } +type UpdateSubscriptionResponse struct { + UpdateSubscriptionResult - if atom.NotFound(err) { - return false, nil - } + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} - return false, err +type UpdateSubscriptionOptions struct { + // For future expansion } // UpdateSubscription updates an existing subscription. -func (ac *AdminClient) UpdateSubscription(ctx context.Context, topicName string, properties *SubscriptionProperties) (*UpdateSubscriptionResponse, error) { - newProps, resp, err := ac.createOrUpdateSubscriptionImpl(ctx, topicName, properties, false) +func (ac *Client) UpdateSubscription(ctx context.Context, topicName string, subscriptionName string, properties SubscriptionProperties, options *UpdateSubscriptionOptions) (*UpdateSubscriptionResponse, error) { + newProps, resp, err := ac.createOrUpdateSubscriptionImpl(ctx, topicName, subscriptionName, &properties, false) if err != nil { return nil, err @@ -300,12 +310,23 @@ func (ac *AdminClient) UpdateSubscription(ctx context.Context, topicName string, return &UpdateSubscriptionResponse{ RawResponse: resp, - Value: newProps, + UpdateSubscriptionResult: UpdateSubscriptionResult{ + SubscriptionProperties: *newProps, + }, }, nil } +type DeleteSubscriptionOptions struct { + // For future expansion +} + +type DeleteSubscriptionResponse struct { + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + // DeleteSubscription deletes a subscription. -func (ac *AdminClient) DeleteSubscription(ctx context.Context, topicName string, subscriptionName string) (*DeleteSubscriptionResponse, error) { +func (ac *Client) DeleteSubscription(ctx context.Context, topicName string, subscriptionName string, options *DeleteSubscriptionOptions) (*DeleteSubscriptionResponse, error) { resp, err := ac.em.Delete(ctx, fmt.Sprintf("/%s/Subscriptions/%s", topicName, subscriptionName)) defer atom.CloseRes(ctx, resp) return &DeleteSubscriptionResponse{ @@ -313,9 +334,9 @@ func (ac *AdminClient) DeleteSubscription(ctx context.Context, topicName string, }, err } -func (ac *AdminClient) createOrUpdateSubscriptionImpl(ctx context.Context, topicName string, props *SubscriptionProperties, creating bool) (*SubscriptionProperties, *http.Response, error) { +func (ac *Client) createOrUpdateSubscriptionImpl(ctx context.Context, topicName string, subscriptionName string, props *SubscriptionProperties, creating bool) (*SubscriptionProperties, *http.Response, error) { if props == nil { - return nil, nil, errors.New("properties are required and cannot be nil") + props = &SubscriptionProperties{} } env := newSubscriptionEnvelope(props, ac.em.TokenProvider()) @@ -332,13 +353,13 @@ func (ac *AdminClient) createOrUpdateSubscriptionImpl(ctx context.Context, topic } var atomResp *atom.SubscriptionEnvelope - resp, err := ac.em.Put(ctx, fmt.Sprintf("/%s/Subscriptions/%s", topicName, props.Name), env, &atomResp, mw...) + resp, err := ac.em.Put(ctx, fmt.Sprintf("/%s/Subscriptions/%s", topicName, subscriptionName), env, &atomResp, mw...) if err != nil { return nil, nil, err } - newProps, err := newSubscriptionProperties(topicName, props.Name, &atomResp.Content.SubscriptionDescription) + newProps, err := newSubscriptionProperties(&atomResp.Content.SubscriptionDescription) if err != nil { return nil, nil, err @@ -359,6 +380,7 @@ func newSubscriptionEnvelope(props *SubscriptionProperties, tokenProvider auth.T ForwardDeadLetteredMessagesTo: props.ForwardDeadLetteredMessagesTo, UserMetadata: props.UserMetadata, EnableBatchedOperations: props.EnableBatchedOperations, + AutoDeleteOnIdle: utils.DurationToStringPtr(props.AutoDeleteOnIdle), // TODO: when we get rule serialization in place. // DefaultRuleDescription: props.DefaultRuleDescription, // are these attributes just not valid anymore? @@ -367,7 +389,7 @@ func newSubscriptionEnvelope(props *SubscriptionProperties, tokenProvider auth.T return atom.WrapWithSubscriptionEnvelope(desc) } -func newSubscriptionProperties(topicName string, subscriptionName string, desc *atom.SubscriptionDescription) (*SubscriptionProperties, error) { +func newSubscriptionProperties(desc *atom.SubscriptionDescription) (*SubscriptionProperties, error) { defaultMessageTimeToLive, err := utils.ISO8601StringToDuration(desc.DefaultMessageTimeToLive) if err != nil { @@ -380,25 +402,30 @@ func newSubscriptionProperties(topicName string, subscriptionName string, desc * return nil, err } + autoDeleteOnIdle, err := utils.ISO8601StringToDuration(desc.AutoDeleteOnIdle) + + if err != nil { + return nil, err + } + return &SubscriptionProperties{ - Name: subscriptionName, - RequiresSession: desc.RequiresSession, - DeadLetteringOnMessageExpiration: desc.DeadLetteringOnMessageExpiration, + RequiresSession: desc.RequiresSession, + DeadLetteringOnMessageExpiration: desc.DeadLetteringOnMessageExpiration, EnableDeadLetteringOnFilterEvaluationExceptions: desc.DeadLetteringOnFilterEvaluationExceptions, - MaxDeliveryCount: desc.MaxDeliveryCount, - ForwardTo: desc.ForwardTo, - ForwardDeadLetteredMessagesTo: desc.ForwardDeadLetteredMessagesTo, - UserMetadata: desc.UserMetadata, - LockDuration: lockDuration, - DefaultMessageTimeToLive: defaultMessageTimeToLive, - EnableBatchedOperations: desc.EnableBatchedOperations, - Status: (*EntityStatus)(desc.Status), + MaxDeliveryCount: desc.MaxDeliveryCount, + ForwardTo: desc.ForwardTo, + ForwardDeadLetteredMessagesTo: desc.ForwardDeadLetteredMessagesTo, + UserMetadata: desc.UserMetadata, + LockDuration: lockDuration, + DefaultMessageTimeToLive: defaultMessageTimeToLive, + EnableBatchedOperations: desc.EnableBatchedOperations, + Status: (*EntityStatus)(desc.Status), + AutoDeleteOnIdle: autoDeleteOnIdle, }, nil } -func newSubscriptionRuntimeProperties(topicName string, subscriptionName string, desc *atom.SubscriptionDescription) *SubscriptionRuntimeProperties { +func newSubscriptionRuntimeProperties(desc *atom.SubscriptionDescription) *SubscriptionRuntimeProperties { return &SubscriptionRuntimeProperties{ - Name: subscriptionName, TotalMessageCount: *desc.MessageCount, ActiveMessageCount: *desc.CountDetails.ActiveMessageCount, DeadLetterMessageCount: *desc.CountDetails.DeadLetterMessageCount, @@ -413,7 +440,7 @@ func newSubscriptionRuntimeProperties(topicName string, subscriptionName string, // subscriptionPropertiesPager provides iteration over SubscriptionProperties pages. type subscriptionPropertiesPager struct { topicName string - innerPager subscriptionFeedPagerFunc + innerPager pagerFunc lastErr error lastResponse *ListSubscriptionsResponse @@ -437,34 +464,38 @@ func (p *subscriptionPropertiesPager) Err() error { } func (p *subscriptionPropertiesPager) getNext(ctx context.Context) (*ListSubscriptionsResponse, error) { - feed, resp, err := p.innerPager(ctx) + var feed *atom.SubscriptionFeed + resp, err := p.innerPager(ctx, &feed) - if err != nil || len(feed.Entries) == 0 { + if err != nil || feed == nil { return nil, err } - var all []*SubscriptionProperties + var all []*SubscriptionPropertiesItem for _, env := range feed.Entries { - props, err := newSubscriptionProperties(p.topicName, env.Title, &env.Content.SubscriptionDescription) + props, err := newSubscriptionProperties(&env.Content.SubscriptionDescription) if err != nil { return nil, err } - all = append(all, props) + all = append(all, &SubscriptionPropertiesItem{ + SubscriptionName: env.Title, + SubscriptionProperties: *props, + }) } return &ListSubscriptionsResponse{ RawResponse: resp, - Value: all, + Items: all, }, nil } // subscriptionRuntimePropertiesPager provides iteration over SubscriptionRuntimeProperties pages. type subscriptionRuntimePropertiesPager struct { topicName string - innerPager subscriptionFeedPagerFunc + innerPager pagerFunc lastErr error lastResponse *ListSubscriptionsRuntimePropertiesResponse @@ -488,45 +519,25 @@ func (p *subscriptionRuntimePropertiesPager) Err() error { } func (p *subscriptionRuntimePropertiesPager) getNextPage(ctx context.Context) (*ListSubscriptionsRuntimePropertiesResponse, error) { - feed, resp, err := p.innerPager(ctx) + var feed *atom.SubscriptionFeed + resp, err := p.innerPager(ctx, &feed) - if err != nil || len(feed.Entries) == 0 { + if err != nil || feed == nil { return nil, err } - var all []*SubscriptionRuntimeProperties + var all []*SubscriptionRuntimePropertiesItem for _, entry := range feed.Entries { - all = append(all, newSubscriptionRuntimeProperties(p.topicName, entry.Title, &entry.Content.SubscriptionDescription)) + all = append(all, &SubscriptionRuntimePropertiesItem{ + TopicName: p.topicName, + SubscriptionName: entry.Title, + SubscriptionRuntimeProperties: *newSubscriptionRuntimeProperties(&entry.Content.SubscriptionDescription), + }) } return &ListSubscriptionsRuntimePropertiesResponse{ RawResponse: resp, - Value: all, + Items: all, }, nil } - -type subscriptionFeedPagerFunc func(ctx context.Context) (*atom.SubscriptionFeed, *http.Response, error) - -func (ac *AdminClient) getSubscriptionPager(topicName string, top int, skip int) subscriptionFeedPagerFunc { - return func(ctx context.Context) (*atom.SubscriptionFeed, *http.Response, error) { - url := fmt.Sprintf("%s/Subscriptions?", topicName) - if top > 0 { - url += fmt.Sprintf("&$top=%d", top) - } - - if skip > 0 { - url += fmt.Sprintf("&$skip=%d", skip) - } - - var atomResp *atom.SubscriptionFeed - resp, err := ac.em.Get(ctx, url, &atomResp) - - if err != nil { - return nil, nil, err - } - - skip += len(atomResp.Entries) - return atomResp, resp, nil - } -} diff --git a/sdk/messaging/azservicebus/admin/admin_client_test.go b/sdk/messaging/azservicebus/admin/admin_client_test.go new file mode 100644 index 000000000000..5af050a40e47 --- /dev/null +++ b/sdk/messaging/azservicebus/admin/admin_client_test.go @@ -0,0 +1,1031 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package admin + +import ( + "context" + "errors" + "fmt" + "net/http" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/atom" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/test" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/utils" + "github.com/stretchr/testify/require" +) + +func TestAdminClient_UsingIdentity(t *testing.T) { + // test with azure identity support + ns := os.Getenv("SERVICEBUS_ENDPOINT") + envCred, err := azidentity.NewEnvironmentCredential(nil) + + if err != nil || ns == "" { + t.Skip("Azure Identity compatible credentials not configured") + } + + adminClient, err := NewClient(ns, envCred, nil) + require.NoError(t, err) + + queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) + props, err := adminClient.CreateQueue(context.Background(), queueName, nil, nil) + require.NoError(t, err) + require.EqualValues(t, 10, *props.MaxDeliveryCount) + + defer func() { + deleteQueue(t, adminClient, queueName) + }() +} + +func TestAdminClient_GetNamespaceProperties(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + resp, err := adminClient.GetNamespaceProperties(context.Background(), nil) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.RawResponse) + + require.True(t, resp.SKU == "Standard" || resp.SKU == "Premium" || resp.SKU == "Basic") + + if resp.SKU == "Standard" || resp.SKU == "Basic" { + // messaging units don't exist in the lower tiers + require.Nil(t, resp.MessagingUnits) + } else { + require.NotNil(t, resp.MessagingUnits) + } + + require.NotEmpty(t, resp.Name) + require.False(t, resp.CreatedTime.IsZero()) + require.False(t, resp.ModifiedTime.IsZero()) +} + +func TestAdminClient_QueueWithMaxValues(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + es := EntityStatusReceiveDisabled + + queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) + + _, err = adminClient.CreateQueue(context.Background(), queueName, &QueueProperties{ + LockDuration: toDurationPtr(45 * time.Second), + // when you enable partitioning Service Bus will automatically create 16 partitions, each with the size + // of MaxSizeInMegabytes. This means when we retrieve this queue we'll get 16*4096 as the size (ie: 64GB) + EnablePartitioning: to.BoolPtr(true), + MaxSizeInMegabytes: to.Int32Ptr(4096), + RequiresDuplicateDetection: to.BoolPtr(true), + RequiresSession: to.BoolPtr(true), + DefaultMessageTimeToLive: toDurationPtr(time.Duration(1<<63 - 1)), + DeadLetteringOnMessageExpiration: to.BoolPtr(true), + DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), + MaxDeliveryCount: to.Int32Ptr(100), + EnableBatchedOperations: to.BoolPtr(false), + Status: &es, + AutoDeleteOnIdle: toDurationPtr(time.Duration(1<<63 - 1)), + UserMetadata: to.StringPtr("some metadata"), + }, nil) + require.NoError(t, err) + + defer deleteQueue(t, adminClient, queueName) + + resp, err := adminClient.GetQueue(context.Background(), queueName) + require.NoError(t, err) + + require.EqualValues(t, QueueProperties{ + LockDuration: toDurationPtr(45 * time.Second), + // ie: this response was from a partitioned queue so the size is the original max size * # of partitions + EnablePartitioning: to.BoolPtr(true), + MaxSizeInMegabytes: to.Int32Ptr(16 * 4096), + RequiresDuplicateDetection: to.BoolPtr(true), + RequiresSession: to.BoolPtr(true), + DefaultMessageTimeToLive: toDurationPtr(time.Duration(1<<63 - 1)), + DeadLetteringOnMessageExpiration: to.BoolPtr(true), + DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), + MaxDeliveryCount: to.Int32Ptr(100), + EnableBatchedOperations: to.BoolPtr(false), + Status: &es, + AutoDeleteOnIdle: toDurationPtr(time.Duration(1<<63 - 1)), + UserMetadata: to.StringPtr("some metadata"), + }, resp.QueueProperties) +} + +func TestAdminClient_CreateQueue(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) + + es := EntityStatusReceiveDisabled + createResp, err := adminClient.CreateQueue(context.Background(), queueName, &QueueProperties{ + LockDuration: toDurationPtr(45 * time.Second), + // when you enable partitioning Service Bus will automatically create 16 partitions, each with the size + // of MaxSizeInMegabytes. This means when we retrieve this queue we'll get 16*4096 as the size (ie: 64GB) + EnablePartitioning: to.BoolPtr(true), + MaxSizeInMegabytes: to.Int32Ptr(4096), + RequiresDuplicateDetection: to.BoolPtr(true), + RequiresSession: to.BoolPtr(true), + DefaultMessageTimeToLive: toDurationPtr(time.Hour * 6), + DeadLetteringOnMessageExpiration: to.BoolPtr(true), + DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), + MaxDeliveryCount: to.Int32Ptr(100), + EnableBatchedOperations: to.BoolPtr(false), + Status: &es, + AutoDeleteOnIdle: toDurationPtr(10 * time.Minute), + }, nil) + require.NoError(t, err) + + defer func() { + deleteQueue(t, adminClient, queueName) + }() + + require.EqualValues(t, QueueProperties{ + LockDuration: toDurationPtr(45 * time.Second), + // ie: this response was from a partitioned queue so the size is the original max size * # of partitions + EnablePartitioning: to.BoolPtr(true), + MaxSizeInMegabytes: to.Int32Ptr(16 * 4096), + RequiresDuplicateDetection: to.BoolPtr(true), + RequiresSession: to.BoolPtr(true), + DefaultMessageTimeToLive: toDurationPtr(time.Hour * 6), + DeadLetteringOnMessageExpiration: to.BoolPtr(true), + DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), + MaxDeliveryCount: to.Int32Ptr(100), + EnableBatchedOperations: to.BoolPtr(false), + Status: &es, + AutoDeleteOnIdle: toDurationPtr(10 * time.Minute), + }, createResp.QueueProperties) + + getResp, err := adminClient.GetQueue(context.Background(), queueName) + require.NoError(t, err) + + require.EqualValues(t, getResp.QueueProperties, createResp.QueueProperties) +} + +func TestAdminClient_UpdateQueue(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) + createdProps, err := adminClient.CreateQueue(context.Background(), queueName, nil, nil) + require.NoError(t, err) + + defer func() { + deleteQueue(t, adminClient, queueName) + }() + + createdProps.MaxDeliveryCount = to.Int32Ptr(101) + updatedProps, err := adminClient.UpdateQueue(context.Background(), queueName, createdProps.QueueProperties, nil) + require.NoError(t, err) + + require.EqualValues(t, 101, *updatedProps.MaxDeliveryCount) + + // try changing a value that's not allowed + updatedProps.RequiresSession = to.BoolPtr(true) + updatedProps, err = adminClient.UpdateQueue(context.Background(), queueName, updatedProps.QueueProperties, nil) + require.Contains(t, err.Error(), "The value for the RequiresSession property of an existing Queue cannot be changed") + require.Nil(t, updatedProps) + + updatedProps, err = adminClient.UpdateQueue(context.Background(), "non-existent-queue", createdProps.QueueProperties, nil) + // a little awkward, we'll make these programatically inspectable as we add in better error handling. + require.Contains(t, err.Error(), "error code: 404") + require.Nil(t, updatedProps) +} + +func TestAdminClient_ListQueues(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + var expectedQueues []string + now := time.Now().UnixNano() + + for i := 0; i < 3; i++ { + queueName := strings.ToLower(fmt.Sprintf("list-queues-%d-%X", i, now)) + expectedQueues = append(expectedQueues, queueName) + + _, err = adminClient.CreateQueue(context.Background(), queueName, &QueueProperties{ + MaxDeliveryCount: to.Int32Ptr(int32(i + 10)), + }, nil) + require.NoError(t, err) + + defer deleteQueue(t, adminClient, queueName) + } + + // we skipped the first queue so it shouldn't come back in the results. + pager := adminClient.ListQueues(&ListQueuesOptions{ + MaxPageSize: 2, + }) + all := map[string]*QueueItem{} + + times := 0 + + for pager.NextPage(context.Background()) { + times++ + page := pager.PageResponse() + + // should never exceed page size + require.LessOrEqual(t, len(page.Items), 2) + + for _, props := range page.Items { + _, exists := all[props.QueueName] + require.False(t, exists, fmt.Sprintf("Each queue result should be unique but found more than one of '%s'", props.QueueName)) + all[props.QueueName] = props + } + } + + require.NoError(t, pager.Err()) + require.GreaterOrEqual(t, times, 2) + + // sanity check - the queues we created exist and their deserialization is + // working. + for i, expectedQueue := range expectedQueues { + props, exists := all[expectedQueue] + require.True(t, exists) + require.EqualValues(t, i+10, *props.MaxDeliveryCount) + } +} + +func TestAdminClient_ListQueuesRuntimeProperties(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + var expectedQueues []string + now := time.Now().UnixNano() + + for i := 0; i < 3; i++ { + queueName := strings.ToLower(fmt.Sprintf("list-queuert-%d-%X", i, now)) + expectedQueues = append(expectedQueues, queueName) + + _, err = adminClient.CreateQueue(context.Background(), queueName, &QueueProperties{ + MaxDeliveryCount: to.Int32Ptr(int32(i + 10)), + }, nil) + require.NoError(t, err) + + defer deleteQueue(t, adminClient, queueName) + } + + // we skipped the first queue so it shouldn't come back in the results. + pager := adminClient.ListQueuesRuntimeProperties(&ListQueuesRuntimePropertiesOptions{ + MaxPageSize: 2, + }) + all := map[string]*QueueRuntimePropertiesItem{} + + times := 0 + + for pager.NextPage(context.Background()) { + times++ + page := pager.PageResponse() + + require.LessOrEqual(t, len(page.Items), 2) + + for _, queueRuntimeItem := range page.Items { + _, exists := all[queueRuntimeItem.QueueName] + require.False(t, exists, fmt.Sprintf("Each queue result should be unique but found more than one of '%s'", queueRuntimeItem.QueueName)) + all[queueRuntimeItem.QueueName] = queueRuntimeItem + } + } + + require.NoError(t, pager.Err()) + require.GreaterOrEqual(t, times, 2) + + // sanity check - the queues we created exist and their deserialization is + // working. + for _, expectedQueue := range expectedQueues { + props, exists := all[expectedQueue] + require.True(t, exists) + require.NotEqualValues(t, time.Time{}, props.CreatedAt) + } +} + +func TestAdminClient_DurationToStringPtr(t *testing.T) { + // The actual value max is TimeSpan.Max so we just assume that's what the user wants if they specify our time.Duration value + require.EqualValues(t, "P10675199DT2H48M5.4775807S", utils.DurationTo8601Seconds(utils.MaxTimeDuration), "Max time.Duration gets converted to TimeSpan.Max") + + require.EqualValues(t, "PT0M1S", utils.DurationTo8601Seconds(time.Second)) + require.EqualValues(t, "PT1M0S", utils.DurationTo8601Seconds(time.Minute)) + require.EqualValues(t, "PT1M1S", utils.DurationTo8601Seconds(time.Minute+time.Second)) + require.EqualValues(t, "PT60M0S", utils.DurationTo8601Seconds(time.Hour)) + require.EqualValues(t, "PT61M1S", utils.DurationTo8601Seconds(time.Hour+time.Minute+time.Second)) +} + +func TestAdminClient_ISO8601StringToDuration(t *testing.T) { + str := "PT10M1S" + duration, err := utils.ISO8601StringToDuration(&str) + require.NoError(t, err) + require.EqualValues(t, (10*time.Minute)+time.Second, *duration) + + duration, err = utils.ISO8601StringToDuration(nil) + require.NoError(t, err) + require.Nil(t, duration) + + str = "PT1S" + duration, err = utils.ISO8601StringToDuration(&str) + require.NoError(t, err) + require.EqualValues(t, time.Second, *duration) + + str = "PT1M" + duration, err = utils.ISO8601StringToDuration(&str) + require.NoError(t, err) + require.EqualValues(t, time.Minute, *duration) + + // this is the .NET timespan max + str = "P10675199DT2H48M5.4775807S" + duration, err = utils.ISO8601StringToDuration(&str) + require.NoError(t, err) + require.EqualValues(t, utils.MaxTimeDuration, *duration) + + // this is the Java equivalent + str = "PT256204778H48M5.4775807S" + duration, err = utils.ISO8601StringToDuration(&str) + require.NoError(t, err) + require.EqualValues(t, utils.MaxTimeDuration, *duration) +} + +func TestAdminClient_TopicAndSubscription(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + topicName := fmt.Sprintf("topic-%X", time.Now().UnixNano()) + subscriptionName := fmt.Sprintf("sub-%X", time.Now().UnixNano()) + forwardToQueueName := fmt.Sprintf("queue-fwd-%X", time.Now().UnixNano()) + + _, err = adminClient.CreateQueue(context.Background(), forwardToQueueName, nil, nil) + require.NoError(t, err) + + defer deleteQueue(t, adminClient, forwardToQueueName) + + status := EntityStatusActive + + // check topic properties, existence + addResp, err := adminClient.CreateTopic(context.Background(), topicName, &TopicProperties{ + EnablePartitioning: to.BoolPtr(true), + MaxSizeInMegabytes: to.Int32Ptr(2048), + RequiresDuplicateDetection: to.BoolPtr(true), + DefaultMessageTimeToLive: toDurationPtr(time.Minute * 3), + DuplicateDetectionHistoryTimeWindow: toDurationPtr(time.Minute * 4), + EnableBatchedOperations: to.BoolPtr(true), + Status: &status, + AutoDeleteOnIdle: toDurationPtr(time.Minute * 7), + SupportOrdering: to.BoolPtr(true), + UserMetadata: to.StringPtr("user metadata"), + }, nil) + require.NoError(t, err) + + defer deleteTopic(t, adminClient, topicName) + + require.EqualValues(t, TopicProperties{ + EnablePartitioning: to.BoolPtr(true), + MaxSizeInMegabytes: to.Int32Ptr(16 * 2048), // enabling partitioning increases our max size because of the 16 partitions + RequiresDuplicateDetection: to.BoolPtr(true), + DefaultMessageTimeToLive: toDurationPtr(time.Minute * 3), + DuplicateDetectionHistoryTimeWindow: toDurationPtr(time.Minute * 4), + EnableBatchedOperations: to.BoolPtr(true), + Status: &status, + AutoDeleteOnIdle: toDurationPtr(time.Minute * 7), + SupportOrdering: to.BoolPtr(true), + UserMetadata: to.StringPtr("user metadata"), + }, addResp.TopicProperties) + + getResp, err := adminClient.GetTopic(context.Background(), topicName, nil) + require.NoError(t, err) + + require.EqualValues(t, TopicProperties{ + EnablePartitioning: to.BoolPtr(true), + MaxSizeInMegabytes: to.Int32Ptr(16 * 2048), // enabling partitioning increases our max size because of the 16 partitions + RequiresDuplicateDetection: to.BoolPtr(true), + DefaultMessageTimeToLive: toDurationPtr(time.Minute * 3), + DuplicateDetectionHistoryTimeWindow: toDurationPtr(time.Minute * 4), + EnableBatchedOperations: to.BoolPtr(true), + Status: &status, + AutoDeleteOnIdle: toDurationPtr(time.Minute * 7), + SupportOrdering: to.BoolPtr(true), + UserMetadata: to.StringPtr("user metadata"), + }, getResp.TopicProperties) + + addSubWithPropsResp, err := adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, &SubscriptionProperties{ + LockDuration: toDurationPtr(3 * time.Minute), + RequiresSession: to.BoolPtr(false), + DefaultMessageTimeToLive: toDurationPtr(7 * time.Minute), + DeadLetteringOnMessageExpiration: to.BoolPtr(true), + EnableDeadLetteringOnFilterEvaluationExceptions: to.BoolPtr(false), + MaxDeliveryCount: to.Int32Ptr(11), + Status: &status, + // ForwardTo: &forwardToQueueName, + // ForwardDeadLetteredMessagesTo: &forwardToQueueName, + EnableBatchedOperations: to.BoolPtr(false), + AutoDeleteOnIdle: toDurationPtr(11 * time.Minute), + UserMetadata: to.StringPtr("user metadata"), + }, nil) + require.NoError(t, err) + + defer deleteSubscription(t, adminClient, topicName, subscriptionName) + + require.EqualValues(t, SubscriptionProperties{ + LockDuration: toDurationPtr(3 * time.Minute), + RequiresSession: to.BoolPtr(false), + DefaultMessageTimeToLive: toDurationPtr(7 * time.Minute), + DeadLetteringOnMessageExpiration: to.BoolPtr(true), + EnableDeadLetteringOnFilterEvaluationExceptions: to.BoolPtr(false), + MaxDeliveryCount: to.Int32Ptr(11), + Status: &status, + // ForwardTo: &forwardToQueueName, + // ForwardDeadLetteredMessagesTo: &forwardToQueueName, + EnableBatchedOperations: to.BoolPtr(false), + AutoDeleteOnIdle: toDurationPtr(11 * time.Minute), + UserMetadata: to.StringPtr("user metadata"), + }, addSubWithPropsResp.CreateSubscriptionResult.SubscriptionProperties) +} + +func TestAdminClient_UpdateTopic(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + topicName := fmt.Sprintf("topic-%X", time.Now().UnixNano()) + addResp, err := adminClient.CreateTopic(context.Background(), topicName, nil, nil) + require.NoError(t, err) + + defer deleteTopic(t, adminClient, topicName) + + addResp.AutoDeleteOnIdle = toDurationPtr(11 * time.Minute) + updateResp, err := adminClient.UpdateTopic(context.Background(), topicName, addResp.TopicProperties, nil) + require.NoError(t, err) + + require.EqualValues(t, 11*time.Minute, *updateResp.AutoDeleteOnIdle) + + // try changing a value that's not allowed + updateResp.EnablePartitioning = to.BoolPtr(true) + updateResp, err = adminClient.UpdateTopic(context.Background(), topicName, updateResp.TopicProperties, nil) + require.Contains(t, err.Error(), "Partitioning cannot be changed for Topic. ") + require.Nil(t, updateResp) + + updateResp, err = adminClient.UpdateTopic(context.Background(), "non-existent-topic", addResp.TopicProperties, nil) + // a little awkward, we'll make these programatically inspectable as we add in better error handling. + require.Contains(t, err.Error(), "error code: 404") + require.Nil(t, updateResp) +} + +func TestAdminClient_ListTopics(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + var expectedTopics []string + now := time.Now().UnixNano() + + wg := sync.WaitGroup{} + + for i := 0; i < 3; i++ { + topicName := strings.ToLower(fmt.Sprintf("list-topic-%d-%X", i, now)) + expectedTopics = append(expectedTopics, topicName) + wg.Add(1) + + go func(i int) { + defer wg.Done() + _, err = adminClient.CreateTopic(context.Background(), topicName, &TopicProperties{ + DefaultMessageTimeToLive: toDurationPtr(time.Duration(i+1) * time.Minute), + }, nil) + require.NoError(t, err) + }(i) + + defer deleteTopic(t, adminClient, topicName) + } + + wg.Wait() + + // we skipped the first topic so it shouldn't come back in the results. + pager := adminClient.ListTopics(&ListTopicsOptions{ + MaxPageSize: 2, + }) + all := map[string]*TopicItem{} + + times := 0 + + for pager.NextPage(context.Background()) { + times++ + page := pager.PageResponse() + + require.LessOrEqual(t, len(page.Items), 2) + + for _, topicItem := range page.Items { + _, exists := all[topicItem.TopicName] + require.False(t, exists, fmt.Sprintf("Each topic result should be unique but found more than one of '%s'", topicItem.TopicName)) + all[topicItem.TopicName] = topicItem + } + } + + require.NoError(t, pager.Err()) + require.GreaterOrEqual(t, times, 2) + + // sanity check - the topics we created exist and their deserialization is + // working. + for i, expectedTopic := range expectedTopics { + props, exists := all[expectedTopic] + require.True(t, exists) + require.EqualValues(t, time.Duration(i+1)*time.Minute, *props.DefaultMessageTimeToLive) + } +} + +func TestAdminClient_ListTopicsRuntimeProperties(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + var expectedTopics []string + now := time.Now().UnixNano() + + for i := 0; i < 3; i++ { + topicName := strings.ToLower(fmt.Sprintf("list-topicrt-%d-%X", i, now)) + expectedTopics = append(expectedTopics, topicName) + + _, err = adminClient.CreateTopic(context.Background(), topicName, nil, nil) + require.NoError(t, err) + + defer deleteTopic(t, adminClient, topicName) + } + + times := 0 + + // we skipped the first topic so it shouldn't come back in the results. + pager := adminClient.ListTopicsRuntimeProperties(&ListTopicsRuntimePropertiesOptions{ + MaxPageSize: 2, + }) + all := map[string]*TopicRuntimePropertiesItem{} + + for pager.NextPage(context.Background()) { + times++ + page := pager.PageResponse() + + require.LessOrEqual(t, len(page.Items), 2) + + for _, item := range page.Items { + _, exists := all[item.TopicName] + require.False(t, exists, fmt.Sprintf("Each topic result should be unique but found more than one of '%s'", item.TopicName)) + all[item.TopicName] = item + } + } + + require.NoError(t, pager.Err()) + require.GreaterOrEqual(t, times, 2) + + // sanity check - the topics we created exist and their deserialization is + // working. + for _, expectedTopic := range expectedTopics { + props, exists := all[expectedTopic] + require.True(t, exists) + require.NotEqualValues(t, time.Time{}, props.CreatedAt) + } +} + +func TestAdminClient_ListSubscriptions(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + now := time.Now().UnixNano() + topicName := strings.ToLower(fmt.Sprintf("listsub-%X", now)) + + _, err = adminClient.CreateTopic(context.Background(), topicName, nil, nil) + require.NoError(t, err) + + defer deleteTopic(t, adminClient, topicName) + + var expectedSubscriptions []string + + for i := 0; i < 3; i++ { + subName := strings.ToLower(fmt.Sprintf("sub-%d-%X", i, now)) + expectedSubscriptions = append(expectedSubscriptions, subName) + + _, err = adminClient.CreateSubscription(context.Background(), topicName, subName, &SubscriptionProperties{ + DefaultMessageTimeToLive: toDurationPtr(time.Duration(i+1) * time.Minute), + }, nil) + require.NoError(t, err) + + defer deleteSubscription(t, adminClient, topicName, subName) + } + + // we skipped the first topic so it shouldn't come back in the results. + pager := adminClient.ListSubscriptions(topicName, &ListSubscriptionsOptions{ + MaxPageSize: 2, + }) + all := map[string]*SubscriptionPropertiesItem{} + + times := 0 + + for pager.NextPage(context.Background()) { + times++ + page := pager.PageResponse() + + require.LessOrEqual(t, len(page.Items), 2) + + for _, item := range page.Items { + _, exists := all[item.SubscriptionName] + require.False(t, exists, fmt.Sprintf("Each subscription result should be unique but found more than one of '%s'", item.SubscriptionName)) + all[item.SubscriptionName] = item + } + } + + require.NoError(t, pager.Err()) + require.GreaterOrEqual(t, times, 2) + + // sanity check - the subscriptions we created exist and their deserialization is + // working. + for i, expectedTopic := range expectedSubscriptions { + props, exists := all[expectedTopic] + require.True(t, exists) + require.EqualValues(t, time.Duration(i+1)*time.Minute, *props.DefaultMessageTimeToLive) + } +} + +func TestAdminClient_ListSubscriptionRuntimeProperties(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + now := time.Now().UnixNano() + topicName := strings.ToLower(fmt.Sprintf("listsubrt-%X", now)) + + _, err = adminClient.CreateTopic(context.Background(), topicName, nil, nil) + require.NoError(t, err) + + var expectedSubs []string + + for i := 0; i < 3; i++ { + subscriptionName := strings.ToLower(fmt.Sprintf("sub-%d-%X", i, now)) + expectedSubs = append(expectedSubs, subscriptionName) + + _, err = adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, nil, nil) + require.NoError(t, err) + + defer deleteSubscription(t, adminClient, topicName, subscriptionName) + } + + // we skipped the first subscription so it shouldn't come back in the results. + pager := adminClient.ListSubscriptionsRuntimeProperties(topicName, &ListSubscriptionsRuntimePropertiesOptions{ + MaxPageSize: 2, + }) + all := map[string]*SubscriptionRuntimePropertiesItem{} + times := 0 + + for pager.NextPage(context.Background()) { + times++ + page := pager.PageResponse() + + require.LessOrEqual(t, len(page.Items), 2) + + for _, subItem := range page.Items { + _, exists := all[subItem.SubscriptionName] + require.False(t, exists, fmt.Sprintf("Each subscription result should be unique but found more than one of '%s'", subItem.SubscriptionName)) + all[subItem.SubscriptionName] = subItem + } + } + + require.NoError(t, pager.Err()) + require.GreaterOrEqual(t, times, 2) + + // sanity check - the topics we created exist and their deserialization is + // working. + for _, expectedSub := range expectedSubs { + props, exists := all[expectedSub] + require.True(t, exists) + require.NotEqualValues(t, time.Time{}, props.CreatedAt) + } +} + +func TestAdminClient_UpdateSubscription(t *testing.T) { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + topicName := fmt.Sprintf("topic-%X", time.Now().UnixNano()) + _, err = adminClient.CreateTopic(context.Background(), topicName, nil, nil) + require.NoError(t, err) + + defer deleteTopic(t, adminClient, topicName) + + subscriptionName := fmt.Sprintf("sub-%X", time.Now().UnixNano()) + addResp, err := adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, nil, nil) + require.NoError(t, err) + + defer deleteSubscription(t, adminClient, topicName, subscriptionName) + + addResp.LockDuration = toDurationPtr(4 * time.Minute) + updateResp, err := adminClient.UpdateSubscription(context.Background(), topicName, subscriptionName, addResp.SubscriptionProperties, nil) + require.NoError(t, err) + + require.EqualValues(t, 4*time.Minute, *updateResp.LockDuration) + + // try changing a value that's not allowed + updateResp.RequiresSession = to.BoolPtr(true) + updateResp, err = adminClient.UpdateSubscription(context.Background(), topicName, subscriptionName, updateResp.SubscriptionProperties, nil) + require.Contains(t, err.Error(), "The value for the RequiresSession property of an existing Subscription cannot be changed") + require.Nil(t, updateResp) + + updateResp, err = adminClient.UpdateSubscription(context.Background(), topicName, "non-existent-subscription", addResp.CreateSubscriptionResult.SubscriptionProperties, nil) + // a little awkward, we'll make these programatically inspectable as we add in better error handling. + require.Contains(t, err.Error(), "error code: 404") + require.Nil(t, updateResp) +} + +func TestAdminClient_LackPermissions_Queue(t *testing.T) { + testData := setupLowPrivTest(t) + defer testData.Cleanup() + + ctx := context.Background() + + _, err := testData.Client.GetQueue(ctx, "not-found-queue") + notFound, resp := atom.NotFound(err) + require.True(t, notFound) + require.NotNil(t, resp) + + _, err = testData.Client.GetQueue(ctx, testData.QueueName) + require.Contains(t, err.Error(), "error code: 401, Details: Manage,EntityRead claims") + + pager := testData.Client.ListQueues(nil) + require.False(t, pager.NextPage(context.Background())) + require.Contains(t, pager.Err().Error(), "error code: 401, Details: Manage,EntityRead claims required for this operation") + + _, err = testData.Client.CreateQueue(ctx, "canneverbecreated", nil, nil) + require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityWrite") + + _, err = testData.Client.UpdateQueue(ctx, "canneverbecreated", QueueProperties{}, nil) + require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityWrite") + + _, err = testData.Client.DeleteQueue(ctx, testData.QueueName, nil) + require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityDelete.") +} + +func TestAdminClient_LackPermissions_Topic(t *testing.T) { + testData := setupLowPrivTest(t) + defer testData.Cleanup() + + ctx := context.Background() + + _, err := testData.Client.GetTopic(ctx, "not-found-topic", nil) + notFound, resp := atom.NotFound(err) + require.True(t, notFound) + require.NotNil(t, resp) + + _, err = testData.Client.GetTopic(ctx, testData.TopicName, nil) + require.Contains(t, err.Error(), "error code: 401, Details: Manage,EntityRead claims") + + pager := testData.Client.ListTopics(nil) + require.False(t, pager.NextPage(context.Background())) + require.Contains(t, pager.Err().Error(), "error code: 401, Details: Manage,EntityRead claims required for this operation") + + _, err = testData.Client.CreateTopic(ctx, "canneverbecreated", nil, nil) + require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action") + + _, err = testData.Client.UpdateTopic(ctx, "canneverbecreated", TopicProperties{}, nil) + require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action") + + _, err = testData.Client.DeleteTopic(ctx, testData.TopicName, nil) + require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityDelete.") + + // sanity check that the http response is getting bundled into these errors, should it be needed. + var httpResponse azcore.HTTPResponse + require.True(t, errors.As(err, &httpResponse)) + require.EqualValues(t, http.StatusUnauthorized, httpResponse.RawResponse().StatusCode) +} + +func TestAdminClient_LackPermissions_Subscription(t *testing.T) { + testData := setupLowPrivTest(t) + defer testData.Cleanup() + + ctx := context.Background() + + _, err := testData.Client.GetSubscription(ctx, testData.TopicName, "not-found-sub", nil) + require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'GetSubscription'") + + _, err = testData.Client.GetSubscription(ctx, testData.TopicName, testData.SubName, nil) + require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'GetSubscription'") + + pager := testData.Client.ListSubscriptions(testData.TopicName, nil) + require.False(t, pager.NextPage(context.Background())) + require.Contains(t, pager.Err().Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'EnumerateSubscriptions' operation") + + _, err = testData.Client.CreateSubscription(ctx, testData.TopicName, "canneverbecreated", nil, nil) + require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'CreateOrUpdateSubscription'") + + _, err = testData.Client.UpdateSubscription(ctx, testData.TopicName, "canneverbecreated", SubscriptionProperties{}, nil) + require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'CreateOrUpdateSubscription'") + + _, err = testData.Client.DeleteSubscription(ctx, testData.TopicName, testData.SubName, nil) + require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'DeleteSubscription'") +} + +type entityManagerForPagerTests struct { + atom.EntityManager + getPaths []string +} + +func (em *entityManagerForPagerTests) Get(ctx context.Context, entityPath string, respObj interface{}, mw ...atom.MiddlewareFunc) (*http.Response, error) { + em.getPaths = append(em.getPaths, entityPath) + + switch feedPtrPtr := respObj.(type) { + case **atom.TopicFeed: + *feedPtrPtr = &atom.TopicFeed{ + Entries: []atom.TopicEnvelope{ + {}, + {}, + {}, + }, + } + default: + panic(fmt.Sprintf("Unknown feed type: %T", respObj)) + } + + return &http.Response{}, nil +} + +func TestAdminClient_pagerWithLightPage(t *testing.T) { + adminClient, err := NewClientFromConnectionString("Endpoint=sb://fakeendpoint.something/;SharedAccessKeyName=fakekeyname;SharedAccessKey=CHANGEME", nil) + require.NoError(t, err) + + em := &entityManagerForPagerTests{} + adminClient.em = em + + pager := adminClient.newPagerFunc("/$Resources/Topics", 10, func(pv interface{}) int { + // note that we're returning fewer results than the max page size + // in ATOM < max page size means this is the last page of results. + return 3 + }) + + var feed *atom.TopicFeed + resp, err := pager(context.Background(), &feed) + + // first page should be good + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, feed) + + require.EqualValues(t, []string{ + "/$Resources/Topics?&$top=10", + }, em.getPaths) + + // but from this point on it's considered EOF since the + // previous page of results was "light" + feed = nil + + resp, err = pager(context.Background(), &feed) + require.Nil(t, resp) + require.Nil(t, err) + require.Nil(t, feed) + + // no new calls will be made to the service + require.EqualValues(t, []string{ + "/$Resources/Topics?&$top=10", + }, em.getPaths) +} + +func TestAdminClient_pagerWithFullPage(t *testing.T) { + adminClient, err := NewClientFromConnectionString("Endpoint=sb://fakeendpoint.something/;SharedAccessKeyName=fakekeyname;SharedAccessKey=CHANGEME", nil) + require.NoError(t, err) + + em := &entityManagerForPagerTests{} + adminClient.em = em + + // first request - got 10 results back, not EOF + simulatedPageSize := 10 + + pager := adminClient.newPagerFunc("/$Resources/Topics", 10, func(pv interface{}) int { + return simulatedPageSize + }) + + var feed *atom.TopicFeed + simulatedPageSize = 10 // pretend the first page was full + resp, err := pager(context.Background(), &feed) + + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, feed) + + require.EqualValues(t, []string{ + "/$Resources/Topics?&$top=10", + }, em.getPaths) + + // grabbing the next page now, we'll also get 10 results back (still not EOF) + simulatedPageSize = 10 + feed = nil + + simulatedPageSize = 10 // pretend the first page was full + resp, err = pager(context.Background(), &feed) + + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, feed) + + require.EqualValues(t, []string{ + "/$Resources/Topics?&$top=10", + "/$Resources/Topics?&$top=10&$skip=10", + }, em.getPaths) + + // we'll return zero results for this next request, which should stop this pager. + simulatedPageSize = 0 + feed = nil + + // nil, nil across the board indicates there wasn't an error, but we're + // definitely done _now_, rather than having to check with another request. + resp, err = pager(context.Background(), &feed) + require.Nil(t, resp) + require.Nil(t, err) + + // no new calls will be made to the service + require.EqualValues(t, []string{ + "/$Resources/Topics?&$top=10", + "/$Resources/Topics?&$top=10&$skip=10", + "/$Resources/Topics?&$top=10&$skip=20", + }, em.getPaths) + + // no more requests will be made, the pager is shut down after EOF. + simulatedPageSize = 10 + feed = nil + + resp, err = pager(context.Background(), &feed) + require.Nil(t, resp) + require.Nil(t, err) + + // no new calls will be made to the service + require.EqualValues(t, []string{ + "/$Resources/Topics?&$top=10", + "/$Resources/Topics?&$top=10&$skip=10", + "/$Resources/Topics?&$top=10&$skip=20", + }, em.getPaths) +} + +func toDurationPtr(d time.Duration) *time.Duration { + return &d +} + +func deleteQueue(t *testing.T, ac *Client, queueName string) { + _, err := ac.DeleteQueue(context.Background(), queueName, nil) + require.NoError(t, err) +} + +func deleteTopic(t *testing.T, ac *Client, topicName string) { + _, err := ac.DeleteTopic(context.Background(), topicName, nil) + require.NoError(t, err) +} + +func deleteSubscription(t *testing.T, ac *Client, topicName string, subscriptionName string) { + _, err := ac.DeleteSubscription(context.Background(), topicName, subscriptionName, nil) + require.NoError(t, err) +} + +func setupLowPrivTest(t *testing.T) *struct { + Client *Client + TopicName string + SubName string + QueueName string + Cleanup func() +} { + adminClient, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) + require.NoError(t, err) + + lowPrivAdminClient, err := NewClientFromConnectionString(test.GetConnectionStringWithoutManagePerms(t), nil) + require.NoError(t, err) + + nanoSeconds := time.Now().UnixNano() + + topicName := fmt.Sprintf("topic-%d", nanoSeconds) + queueName := fmt.Sprintf("queue-%d", nanoSeconds) + subName := "subscription1" + + // TODO: add in rule management + //ruleName := "rule" + + // create some entities that we need (there's a diff between something not being + // found and something failing because of lack of authorization) + cleanup := func() func() { + _, err = adminClient.CreateQueue(context.Background(), queueName, nil, nil) + require.NoError(t, err) + + _, err = adminClient.CreateTopic(context.Background(), topicName, nil, nil) + require.NoError(t, err) + + _, err = adminClient.CreateSubscription(context.Background(), topicName, subName, nil, nil) + require.NoError(t, err) + + // _, err = sm.PutRule(context.Background(), subName, ruleName, TrueFilter{}) + // require.NoError(t, err) + + return func() { + deleteTopic(t, adminClient, topicName) // will also delete the subscription + deleteQueue(t, adminClient, queueName) + } + }() + + return &struct { + Client *Client + TopicName string + SubName string + QueueName string + Cleanup func() + }{ + Client: lowPrivAdminClient, + QueueName: queueName, + TopicName: topicName, + SubName: subName, + Cleanup: cleanup, + } +} diff --git a/sdk/messaging/azservicebus/admin_client_topic.go b/sdk/messaging/azservicebus/admin/admin_client_topic.go similarity index 67% rename from sdk/messaging/azservicebus/admin_client_topic.go rename to sdk/messaging/azservicebus/admin/admin_client_topic.go index 82dc2f5de3a6..28d87a469684 100644 --- a/sdk/messaging/azservicebus/admin_client_topic.go +++ b/sdk/messaging/azservicebus/admin/admin_client_topic.go @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package azservicebus +package admin import ( "context" - "errors" - "fmt" "net/http" "time" @@ -17,9 +15,6 @@ import ( // TopicProperties represents the static properties of the topic. type TopicProperties struct { - // Name of the topic relative to the namespace base address. - Name string - // MaxSizeInMegabytes - The maximum size of the topic in megabytes, which is the size of memory // allocated for the topic. // Default is 1024. @@ -49,16 +44,16 @@ type TopicProperties struct { // EnablePartitioning indicates whether the topic is to be partitioned across multiple message brokers. EnablePartitioning *bool - // SupportsOrdering defines whether ordering needs to be maintained. If true, messages + // SupportOrdering defines whether ordering needs to be maintained. If true, messages // sent to topic will be forwarded to the subscription, in order. - SupportsOrdering *bool + SupportOrdering *bool + + // UserMetadata is custom metadata that user can associate with the topic. + UserMetadata *string } // TopicRuntimeProperties represent dynamic properties of a topic, such as the ActiveMessageCount. type TopicRuntimeProperties struct { - // Name is the name of the topic. - Name string - // SizeInBytes - The size of the topic, in bytes. SizeInBytes int64 @@ -78,78 +73,54 @@ type TopicRuntimeProperties struct { ScheduledMessageCount int32 } -type AddTopicResponse struct { - // Value is the result of the request. - Value *TopicProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type CreateTopicResult struct { + TopicProperties } -type GetTopicResponse struct { - // Value is the result of the request. - Value *TopicProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response -} +type CreateTopicResponse struct { + CreateTopicResult -type GetTopicRuntimePropertiesResponse struct { - // Value is the result of the request. - Value *TopicRuntimeProperties // RawResponse is the *http.Response for the request. RawResponse *http.Response } -type ListTopicsResponse struct { - // Value is the result of the request. - Value []*TopicProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response +type CreateTopicOptions struct { + // For future expansion } -type ListTopicsRuntimePropertiesResponse struct { - // Value is the result of the request. - Value []*TopicRuntimeProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response -} +// CreateTopic creates a topic using defaults for all options. +func (ac *Client) CreateTopic(ctx context.Context, topicName string, properties *TopicProperties, options *CreateTopicOptions) (*CreateTopicResponse, error) { + newProps, resp, err := ac.createOrUpdateTopicImpl(ctx, topicName, properties, true) -type UpdateTopicResponse struct { - // Value is the result of the request. - Value *TopicProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response -} + if err != nil { + return nil, err + } -type DeleteTopicResponse struct { - // Value is the result of the request. - Value *TopicProperties - // RawResponse is the *http.Response for the request. - RawResponse *http.Response + return &CreateTopicResponse{ + RawResponse: resp, + CreateTopicResult: CreateTopicResult{ + TopicProperties: *newProps, + }, + }, nil } -// AddTopic creates a topic using defaults for all options. -func (ac *AdminClient) AddTopic(ctx context.Context, topicName string) (*AddTopicResponse, error) { - return ac.AddTopicWithProperties(ctx, &TopicProperties{ - Name: topicName, - }) +type GetTopicResult struct { + TopicProperties } -// AddTopicWithProperties creates a topic with configurable properties -func (ac *AdminClient) AddTopicWithProperties(ctx context.Context, properties *TopicProperties) (*AddTopicResponse, error) { - props, resp, err := ac.createOrUpdateTopicImpl(ctx, properties, true) +type GetTopicResponse struct { + GetTopicResult - if err != nil { - return nil, err - } + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} - return &AddTopicResponse{ - RawResponse: resp, - Value: props, - }, nil +type GetTopicOptions struct { + // For future expansion } // GetTopic gets a topic by name. -func (ac *AdminClient) GetTopic(ctx context.Context, topicName string) (*GetTopicResponse, error) { +func (ac *Client) GetTopic(ctx context.Context, topicName string, options *GetTopicOptions) (*GetTopicResponse, error) { var atomResp *atom.TopicEnvelope resp, err := ac.em.Get(ctx, "/"+topicName, &atomResp) @@ -157,7 +128,7 @@ func (ac *AdminClient) GetTopic(ctx context.Context, topicName string) (*GetTopi return nil, err } - props, err := newTopicProperties(atomResp.Title, &atomResp.Content.TopicDescription) + props, err := newTopicProperties(&atomResp.Content.TopicDescription) if err != nil { return nil, err @@ -165,12 +136,30 @@ func (ac *AdminClient) GetTopic(ctx context.Context, topicName string) (*GetTopi return &GetTopicResponse{ RawResponse: resp, - Value: props, + GetTopicResult: GetTopicResult{ + TopicProperties: *props, + }, }, nil } +type GetTopicRuntimePropertiesResult struct { + // Value is the result of the request. + TopicRuntimeProperties +} + +type GetTopicRuntimePropertiesResponse struct { + GetTopicRuntimePropertiesResult + + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + +type GetTopicRuntimePropertiesOptions struct { + // For future expansion +} + // GetTopicRuntimeProperties gets runtime properties of a topic, like the SizeInBytes, or SubscriptionCount. -func (ac *AdminClient) GetTopicRuntimeProperties(ctx context.Context, topicName string) (*GetTopicRuntimePropertiesResponse, error) { +func (ac *Client) GetTopicRuntimeProperties(ctx context.Context, topicName string, options *GetTopicRuntimePropertiesOptions) (*GetTopicRuntimePropertiesResponse, error) { var atomResp *atom.TopicEnvelope resp, err := ac.em.Get(ctx, "/"+topicName, &atomResp) @@ -180,16 +169,29 @@ func (ac *AdminClient) GetTopicRuntimeProperties(ctx context.Context, topicName return &GetTopicRuntimePropertiesResponse{ RawResponse: resp, - Value: newTopicRuntimeProperties(atomResp.Title, &atomResp.Content.TopicDescription), + GetTopicRuntimePropertiesResult: GetTopicRuntimePropertiesResult{ + TopicRuntimeProperties: *newTopicRuntimeProperties(&atomResp.Content.TopicDescription), + }, }, nil } +type TopicItem struct { + TopicProperties + + TopicName string +} + +type ListTopicsResponse struct { + // Items is the result of the request. + Items []*TopicItem + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + // ListTopicsOptions can be used to configure the ListTopics method. type ListTopicsOptions struct { - // Top is the maximum size of each page of results. - Top int - // Skip is the starting index for the paging operation. - Skip int + // MaxPageSize is the maximum size of each page of results. + MaxPageSize int32 } // TopicPropertiesPager provides iteration over ListTopicProperties pages. @@ -206,26 +208,37 @@ type TopicPropertiesPager interface { } // ListTopics lists topics. -func (ac *AdminClient) ListTopics(options *ListTopicsOptions) TopicPropertiesPager { - var pageSize int - var skip int +func (ac *Client) ListTopics(options *ListTopicsOptions) TopicPropertiesPager { + var pageSize int32 if options != nil { - skip = options.Skip - pageSize = options.Top + pageSize = options.MaxPageSize } + pagerFunc := ac.newPagerFunc("/$Resources/Topics", pageSize, topicFeedLen) + return &topicPropertiesPager{ - innerPager: ac.getTopicPager(pageSize, skip), + innerPager: pagerFunc, } } +type TopicRuntimePropertiesItem struct { + TopicRuntimeProperties + + TopicName string +} + +type ListTopicsRuntimePropertiesResponse struct { + // Items is the result of the request. + Items []*TopicRuntimePropertiesItem + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + // ListTopicsRuntimePropertiesOptions can be used to configure the ListTopicsRuntimeProperties method. type ListTopicsRuntimePropertiesOptions struct { - // Top is the maximum size of each page of results. - Top int - // Skip is the starting index for the paging operation. - Skip int + // MaxPageSize is the maximum size of each page of results. + MaxPageSize int32 } // TopicRuntimePropertiesPager provides iteration over ListTopicRuntimeProperties pages. @@ -242,41 +255,38 @@ type TopicRuntimePropertiesPager interface { } // ListTopicsRuntimeProperties lists runtime properties for topics. -func (ac *AdminClient) ListTopicsRuntimeProperties(options *ListTopicsRuntimePropertiesOptions) TopicRuntimePropertiesPager { - var pageSize int - var skip int +func (ac *Client) ListTopicsRuntimeProperties(options *ListTopicsRuntimePropertiesOptions) TopicRuntimePropertiesPager { + var pageSize int32 if options != nil { - skip = options.Skip - pageSize = options.Top + pageSize = options.MaxPageSize } + pagerFunc := ac.newPagerFunc("/$Resources/Topics", pageSize, topicFeedLen) + return &topicRuntimePropertiesPager{ - innerPager: ac.getTopicPager(pageSize, skip), + innerPager: pagerFunc, } } -// TopicExists checks if a topic exists. -// Returns true if the topic is found -// (false, nil) if the topic is not found -// (false, err) if an error occurred while trying to check if the topic exists. -func (ac *AdminClient) TopicExists(ctx context.Context, topicName string) (bool, error) { - _, err := ac.GetTopic(ctx, topicName) +type UpdateTopicResult struct { + TopicProperties +} - if err == nil { - return true, nil - } +type UpdateTopicResponse struct { + UpdateTopicResult - if atom.NotFound(err) { - return false, nil - } + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} - return false, err +type UpdateTopicOptions struct { + // For future expansion } // UpdateTopic updates an existing topic. -func (ac *AdminClient) UpdateTopic(ctx context.Context, properties *TopicProperties) (*UpdateTopicResponse, error) { - newProps, resp, err := ac.createOrUpdateTopicImpl(ctx, properties, false) +func (ac *Client) UpdateTopic(ctx context.Context, topicName string, properties TopicProperties, options *UpdateTopicOptions) (*UpdateTopicResponse, error) { + newProps, resp, err := ac.createOrUpdateTopicImpl(ctx, topicName, &properties, false) if err != nil { return nil, err @@ -284,12 +294,25 @@ func (ac *AdminClient) UpdateTopic(ctx context.Context, properties *TopicPropert return &UpdateTopicResponse{ RawResponse: resp, - Value: newProps, + UpdateTopicResult: UpdateTopicResult{ + TopicProperties: *newProps, + }, }, nil } +type DeleteTopicResponse struct { + // Value is the result of the request. + Value *TopicProperties + // RawResponse is the *http.Response for the request. + RawResponse *http.Response +} + +type DeleteTopicOptions struct { + // For future expansion +} + // DeleteTopic deletes a topic. -func (ac *AdminClient) DeleteTopic(ctx context.Context, topicName string) (*DeleteTopicResponse, error) { +func (ac *Client) DeleteTopic(ctx context.Context, topicName string, options *DeleteTopicOptions) (*DeleteTopicResponse, error) { resp, err := ac.em.Delete(ctx, "/"+topicName) defer atom.CloseRes(ctx, resp) return &DeleteTopicResponse{ @@ -297,36 +320,9 @@ func (ac *AdminClient) DeleteTopic(ctx context.Context, topicName string) (*Dele }, err } -func (ac *AdminClient) getTopicPager(top int, skip int) topicFeedPagerFunc { - return func(ctx context.Context) (*atom.TopicFeed, *http.Response, error) { - url := "/$Resources/Topics?" - if top > 0 { - url += fmt.Sprintf("&$top=%d", top) - } - - if skip > 0 { - url += fmt.Sprintf("&$skip=%d", skip) - } - - var atomResp *atom.TopicFeed - resp, err := ac.em.Get(ctx, url, &atomResp) - - if err != nil { - return nil, nil, err - } - - if len(atomResp.Entries) == 0 { - return nil, nil, nil - } - - skip += len(atomResp.Entries) - return atomResp, resp, nil - } -} - -func (ac *AdminClient) createOrUpdateTopicImpl(ctx context.Context, props *TopicProperties, creating bool) (*TopicProperties, *http.Response, error) { +func (ac *Client) createOrUpdateTopicImpl(ctx context.Context, topicName string, props *TopicProperties, creating bool) (*TopicProperties, *http.Response, error) { if props == nil { - return nil, nil, errors.New("properties are required and cannot be nil") + props = &TopicProperties{} } env := newTopicEnvelope(props, ac.em.TokenProvider()) @@ -344,13 +340,13 @@ func (ac *AdminClient) createOrUpdateTopicImpl(ctx context.Context, props *Topic } var atomResp *atom.TopicEnvelope - resp, err := ac.em.Put(ctx, "/"+props.Name, env, &atomResp, mw...) + resp, err := ac.em.Put(ctx, "/"+topicName, env, &atomResp, mw...) if err != nil { return nil, nil, err } - topicProps, err := newTopicProperties(props.Name, &atomResp.Content.TopicDescription) + topicProps, err := newTopicProperties(&atomResp.Content.TopicDescription) if err != nil { return nil, nil, err @@ -368,7 +364,8 @@ func newTopicEnvelope(props *TopicProperties, tokenProvider auth.TokenProvider) EnableBatchedOperations: props.EnableBatchedOperations, Status: (*atom.EntityStatus)(props.Status), - SupportOrdering: props.SupportsOrdering, + UserMetadata: props.UserMetadata, + SupportOrdering: props.SupportOrdering, AutoDeleteOnIdle: utils.DurationToStringPtr(props.AutoDeleteOnIdle), EnablePartitioning: props.EnablePartitioning, } @@ -376,7 +373,7 @@ func newTopicEnvelope(props *TopicProperties, tokenProvider auth.TokenProvider) return atom.WrapWithTopicEnvelope(desc) } -func newTopicProperties(name string, td *atom.TopicDescription) (*TopicProperties, error) { +func newTopicProperties(td *atom.TopicDescription) (*TopicProperties, error) { defaultMessageTimeToLive, err := utils.ISO8601StringToDuration(td.DefaultMessageTimeToLive) if err != nil { @@ -396,22 +393,21 @@ func newTopicProperties(name string, td *atom.TopicDescription) (*TopicPropertie } return &TopicProperties{ - Name: name, MaxSizeInMegabytes: td.MaxSizeInMegabytes, RequiresDuplicateDetection: td.RequiresDuplicateDetection, DefaultMessageTimeToLive: defaultMessageTimeToLive, DuplicateDetectionHistoryTimeWindow: duplicateDetectionHistoryTimeWindow, EnableBatchedOperations: td.EnableBatchedOperations, Status: (*EntityStatus)(td.Status), + UserMetadata: td.UserMetadata, AutoDeleteOnIdle: autoDeleteOnIdle, EnablePartitioning: td.EnablePartitioning, - SupportsOrdering: td.SupportOrdering, + SupportOrdering: td.SupportOrdering, }, nil } -func newTopicRuntimeProperties(name string, desc *atom.TopicDescription) *TopicRuntimeProperties { +func newTopicRuntimeProperties(desc *atom.TopicDescription) *TopicRuntimeProperties { return &TopicRuntimeProperties{ - Name: name, SizeInBytes: int64OrZero(desc.SizeInBytes), CreatedAt: dateTimeToTime(desc.CreatedAt), UpdatedAt: dateTimeToTime(desc.UpdatedAt), @@ -423,7 +419,7 @@ func newTopicRuntimeProperties(name string, desc *atom.TopicDescription) *TopicR // topicPropertiesPager provides iteration over TopicProperties pages. type topicPropertiesPager struct { - innerPager topicFeedPagerFunc + innerPager pagerFunc lastErr error lastResponse *ListTopicsResponse @@ -447,35 +443,37 @@ func (p *topicPropertiesPager) Err() error { } func (p *topicPropertiesPager) getNextPage(ctx context.Context) (*ListTopicsResponse, error) { - feed, resp, err := p.innerPager(ctx) + var feed *atom.TopicFeed + resp, err := p.innerPager(ctx, &feed) if err != nil || feed == nil { return nil, err } - var all []*TopicProperties + var all []*TopicItem for _, env := range feed.Entries { - props, err := newTopicProperties(env.Title, &env.Content.TopicDescription) + props, err := newTopicProperties(&env.Content.TopicDescription) if err != nil { return nil, err } - all = append(all, props) + all = append(all, &TopicItem{ + TopicProperties: *props, + TopicName: env.Title, + }) } return &ListTopicsResponse{ RawResponse: resp, - Value: all, + Items: all, }, nil } -type topicFeedPagerFunc func(ctx context.Context) (*atom.TopicFeed, *http.Response, error) - // topicRuntimePropertiesPager provides iteration over TopicRuntimeProperties pages. type topicRuntimePropertiesPager struct { - innerPager topicFeedPagerFunc + innerPager pagerFunc lastErr error lastResponse *ListTopicsRuntimePropertiesResponse } @@ -488,21 +486,25 @@ func (p *topicRuntimePropertiesPager) NextPage(ctx context.Context) bool { } func (p *topicRuntimePropertiesPager) getNextPage(ctx context.Context) (*ListTopicsRuntimePropertiesResponse, error) { - feed, resp, err := p.innerPager(ctx) + var feed *atom.TopicFeed + resp, err := p.innerPager(ctx, &feed) if err != nil || feed == nil { return nil, err } - var all []*TopicRuntimeProperties + var all []*TopicRuntimePropertiesItem for _, entry := range feed.Entries { - all = append(all, newTopicRuntimeProperties(entry.Title, &entry.Content.TopicDescription)) + all = append(all, &TopicRuntimePropertiesItem{ + TopicName: entry.Title, + TopicRuntimeProperties: *newTopicRuntimeProperties(&entry.Content.TopicDescription), + }) } return &ListTopicsRuntimePropertiesResponse{ RawResponse: resp, - Value: all, + Items: all, }, nil } @@ -515,3 +517,8 @@ func (p *topicRuntimePropertiesPager) PageResponse() *ListTopicsRuntimePropertie func (p *topicRuntimePropertiesPager) Err() error { return p.lastErr } + +func topicFeedLen(pv interface{}) int { + topicFeed := pv.(**atom.TopicFeed) + return len((*topicFeed).Entries) +} diff --git a/sdk/messaging/azservicebus/admin/doc.go b/sdk/messaging/azservicebus/admin/doc.go new file mode 100644 index 000000000000..32ca1f2e3aa7 --- /dev/null +++ b/sdk/messaging/azservicebus/admin/doc.go @@ -0,0 +1,11 @@ +//go:build go1.16 +// +build go1.16 + +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Package admin provides Client, which can create and manage Queues, Topics and +// Subscriptions. +// NOTE: For sending and receiving messages with Azure ServiceBus use the +// `azservicebus.Client` instead. +package admin diff --git a/sdk/messaging/azservicebus/admin/example_admin_client_test.go b/sdk/messaging/azservicebus/admin/example_admin_client_test.go new file mode 100644 index 000000000000..3623a7ecaece --- /dev/null +++ b/sdk/messaging/azservicebus/admin/example_admin_client_test.go @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package admin_test + +import ( + "context" + "fmt" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" +) + +func ExampleNewClient() { + // NOTE: If you'd like to authenticate using a Service Bus connection string + // look at `NewClientWithConnectionString` instead. + + credential, err := azidentity.NewDefaultAzureCredential(nil) + exitOnError("Failed to create a DefaultAzureCredential", err) + + adminClient, err = admin.NewClient("", credential, nil) + exitOnError("Failed to create ServiceBusClient in example", err) +} + +func ExampleNewClientFromConnectionString() { + // NOTE: If you'd like to authenticate via Azure Active Directory look at + // the `NewClient` function instead. + + adminClient, err = admin.NewClientFromConnectionString("", nil) + exitOnError("Failed to create ServiceBusClient in example", err) +} + +func ExampleClient_CreateQueue() { + resp, err := adminClient.CreateQueue(context.TODO(), "queue-name", nil, nil) + exitOnError("Failed to add queue", err) + + // some example properties + fmt.Printf("Max message delivery count = %d", resp.MaxDeliveryCount) + fmt.Printf("Lock duration: %s", resp.LockDuration) +} + +func ExampleClient_CreateQueue_usingproperties() { + lockDuration := time.Minute + maxDeliveryCount := int32(10) + + resp, err := adminClient.CreateQueue(context.TODO(), "queue-name", &admin.QueueProperties{ + // some example properties + LockDuration: &lockDuration, + MaxDeliveryCount: &maxDeliveryCount, + }, nil) + exitOnError("Failed to create queue", err) + + // some example properties + fmt.Printf("Max message delivery count = %d", resp.MaxDeliveryCount) + fmt.Printf("Lock duration: %s", resp.LockDuration) +} + +func ExampleClient_ListQueues() { + queuePager := adminClient.ListQueues(nil) + + for queuePager.NextPage(context.TODO()) { + for _, queue := range queuePager.PageResponse().Items { + fmt.Printf("Queue name: %s, max size in MB: %d", queue.QueueName, queue.MaxSizeInMegabytes) + } + } + + exitOnError("Failed when listing queues", queuePager.Err()) +} + +func ExampleClient_ListQueuesRuntimeProperties() { + queuePager := adminClient.ListQueuesRuntimeProperties(nil) + + for queuePager.NextPage(context.TODO()) { + for _, queue := range queuePager.PageResponse().Items { + fmt.Printf("Queue name: %s, active messages: %d", queue.QueueName, queue.ActiveMessageCount) + } + } + + exitOnError("Failed when listing queues runtime properties", queuePager.Err()) +} + +// NOTE: these are just here to keep the examples succinct. +var adminClient *admin.Client +var err error + +func exitOnError(message string, err error) {} diff --git a/sdk/messaging/azservicebus/admin/main_test.go b/sdk/messaging/azservicebus/admin/main_test.go new file mode 100644 index 000000000000..ba67fac5599b --- /dev/null +++ b/sdk/messaging/azservicebus/admin/main_test.go @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package admin + +import ( + "log" + "os" + "testing" + + "github.com/joho/godotenv" +) + +func TestMain(m *testing.M) { + err := godotenv.Load("../.env") + + if err != nil { + log.Printf("Failed to load env file, no live tests will run: %s", err.Error()) + } + + // call flag.Parse() here if TestMain uses flags + os.Exit(m.Run()) +} diff --git a/sdk/messaging/azservicebus/admin_client.go b/sdk/messaging/azservicebus/admin_client.go deleted file mode 100644 index da1db47c4249..000000000000 --- a/sdk/messaging/azservicebus/admin_client.go +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package azservicebus - -import ( - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal" - "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/atom" -) - -// AdminClient allows you to administer resources in a Service Bus Namespace. -// For example, you can create queues, enabling capabilities like partitioning, duplicate detection, etc.. -// NOTE: For sending and receiving messages you'll need to use the `Client` type instead. -type AdminClient struct { - em *atom.EntityManager -} - -type AdminClientOptions struct { - // for future expansion -} - -// NewAdminClient creates an AdminClient authenticating using a connection string. -func NewAdminClientWithConnectionString(connectionString string, options *AdminClientOptions) (*AdminClient, error) { - em, err := atom.NewEntityManagerWithConnectionString(connectionString, internal.Version) - - if err != nil { - return nil, err - } - - return &AdminClient{em: em}, nil -} - -// NewAdminClient creates an AdminClient authenticating using a TokenCredential. -func NewAdminClient(fullyQualifiedNamespace string, tokenCredential azcore.TokenCredential, options *AdminClientOptions) (*AdminClient, error) { - em, err := atom.NewEntityManager(fullyQualifiedNamespace, tokenCredential, internal.Version) - - if err != nil { - return nil, err - } - - return &AdminClient{em: em}, nil -} - -// func (ac *AdminClient) GetNamespaceProperties() {} diff --git a/sdk/messaging/azservicebus/admin_client_models.go b/sdk/messaging/azservicebus/admin_client_models.go deleted file mode 100644 index 15db7af7b0ea..000000000000 --- a/sdk/messaging/azservicebus/admin_client_models.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package azservicebus - -import ( - "time" -) - -// EntityStatus represents the current status of the entity. -type EntityStatus string - -const ( - // EntityStatusActive indicates an entity can be used for sending and receiving. - EntityStatusActive EntityStatus = "Active" - // EntityStatusDisabled indicates an entity cannot be used for sending or receiving. - EntityStatusDisabled EntityStatus = "Disabled" - // EntityStatusSendDisabled indicates that an entity cannot be used for sending. - EntityStatusSendDisabled EntityStatus = "SendDisabled" - // EntityStatusReceiveDisabled indicates that an entity cannot be used for receiving. - EntityStatusReceiveDisabled EntityStatus = "ReceiveDisabled" -) - -// AccessRights represents the rights for an authorization rule. -type AccessRights int - -const ( - // AccessRightsManage allows management of entities. - AccessRightsManage AccessRights = 0 - // AccessRightsSend allows sending to entities. - AccessRightsSend AccessRights = 1 - // AccessRightsListen allows listening to entities. - AccessRightsListen AccessRights = 2 -) - -// EntityAvailabilityStatus is the availability status of the entity. -type EntityAvailabilityStatus string - -const ( - // EntityAvailabilityStatusAvailable indicates the entity is available. - EntityAvailabilityStatusAvailable EntityAvailabilityStatus = "Available" - - EntityAvailabilityStatusLimited EntityAvailabilityStatus = "Limited" - - // EntityAvailabilityStatusRenaming indicates the entity is being renamed. - EntityAvailabilityStatusRenaming EntityAvailabilityStatus = "Renaming" - - // EntityAvailabilityStatusRestoring indicates the entity is being restored. - EntityAvailabilityStatusRestoring EntityAvailabilityStatus = "Restoring" - - EntityAvailabilityStatusUnknown EntityAvailabilityStatus = "Unknown" -) - -// AuthorizationRule encompasses access rights, metadata and a key for authentication. -type AuthorizationRule struct { - ClaimType string - Rights []AccessRights - KeyName string - CreatedTime time.Time - ModifiedTime time.Time -} diff --git a/sdk/messaging/azservicebus/admin_client_test.go b/sdk/messaging/azservicebus/admin_client_test.go index e02c999d6947..83dfac5ce412 100644 --- a/sdk/messaging/azservicebus/admin_client_test.go +++ b/sdk/messaging/azservicebus/admin_client_test.go @@ -5,199 +5,60 @@ package azservicebus import ( "context" - "errors" "fmt" - "net/http" - "os" - "strings" - "sync" "testing" "time" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/atom" - "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/utils" + "github.com/Azure/azure-amqp-common-go/v3/conn" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/test" "github.com/stretchr/testify/require" ) -func TestAdminClient_UsingIdentity(t *testing.T) { - // test with azure identity support - ns := os.Getenv("SERVICEBUS_ENDPOINT") - envCred, err := azidentity.NewEnvironmentCredential(nil) - - if err != nil || ns == "" { - t.Skip("Azure Identity compatible credentials not configured") - } - - adminClient, err := NewAdminClient(ns, envCred, nil) - require.NoError(t, err) - - queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) - props, err := adminClient.AddQueue(context.Background(), queueName) - require.NoError(t, err) - require.EqualValues(t, queueName, props.Value.Name) - - defer func() { - _, err := adminClient.DeleteQueue(context.Background(), queueName) - require.NoError(t, err) - }() -} - -func TestAdminClient_QueueWithMaxValues(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - es := EntityStatusReceiveDisabled - - queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) - - queueExists, err := adminClient.QueueExists(context.Background(), queueName) - require.False(t, queueExists) - require.NoError(t, err) - - _, err = adminClient.AddQueueWithProperties(context.Background(), &QueueProperties{ - Name: queueName, - LockDuration: toDurationPtr(45 * time.Second), - // when you enable partitioning Service Bus will automatically create 16 partitions, each with the size - // of MaxSizeInMegabytes. This means when we retrieve this queue we'll get 16*4096 as the size (ie: 64GB) - EnablePartitioning: to.BoolPtr(true), - MaxSizeInMegabytes: to.Int32Ptr(4096), - RequiresDuplicateDetection: to.BoolPtr(true), - RequiresSession: to.BoolPtr(true), - DefaultMessageTimeToLive: toDurationPtr(time.Duration(1<<63 - 1)), - DeadLetteringOnMessageExpiration: to.BoolPtr(true), - DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), - MaxDeliveryCount: to.Int32Ptr(100), - EnableBatchedOperations: to.BoolPtr(false), - Status: &es, - AutoDeleteOnIdle: toDurationPtr(time.Duration(1<<63 - 1)), - }) - require.NoError(t, err) - - defer deleteQueue(t, adminClient, queueName) - - queueExists, err = adminClient.QueueExists(context.Background(), queueName) - require.True(t, queueExists) - require.NoError(t, err) - - resp, err := adminClient.GetQueue(context.Background(), queueName) - require.NoError(t, err) - - require.EqualValues(t, &QueueProperties{ - Name: queueName, - LockDuration: toDurationPtr(45 * time.Second), - // ie: this response was from a partitioned queue so the size is the original max size * # of partitions - EnablePartitioning: to.BoolPtr(true), - MaxSizeInMegabytes: to.Int32Ptr(16 * 4096), - RequiresDuplicateDetection: to.BoolPtr(true), - RequiresSession: to.BoolPtr(true), - DefaultMessageTimeToLive: toDurationPtr(time.Duration(1<<63 - 1)), - DeadLetteringOnMessageExpiration: to.BoolPtr(true), - DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), - MaxDeliveryCount: to.Int32Ptr(100), - EnableBatchedOperations: to.BoolPtr(false), - Status: &es, - AutoDeleteOnIdle: toDurationPtr(time.Duration(1<<63 - 1)), - }, resp.Value) -} - -func TestAdminClient_AddQueue(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) - - es := EntityStatusReceiveDisabled - createResp, err := adminClient.AddQueueWithProperties(context.Background(), &QueueProperties{ - Name: queueName, - LockDuration: toDurationPtr(45 * time.Second), - // when you enable partitioning Service Bus will automatically create 16 partitions, each with the size - // of MaxSizeInMegabytes. This means when we retrieve this queue we'll get 16*4096 as the size (ie: 64GB) - EnablePartitioning: to.BoolPtr(true), - MaxSizeInMegabytes: to.Int32Ptr(4096), - RequiresDuplicateDetection: to.BoolPtr(true), - RequiresSession: to.BoolPtr(true), - DefaultMessageTimeToLive: toDurationPtr(time.Hour * 6), - DeadLetteringOnMessageExpiration: to.BoolPtr(true), - DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), - MaxDeliveryCount: to.Int32Ptr(100), - EnableBatchedOperations: to.BoolPtr(false), - Status: &es, - AutoDeleteOnIdle: toDurationPtr(10 * time.Minute), - }) - require.NoError(t, err) - - defer func() { - _, err := adminClient.DeleteQueue(context.Background(), queueName) - require.NoError(t, err) - }() - - require.EqualValues(t, &QueueProperties{ - Name: queueName, - LockDuration: toDurationPtr(45 * time.Second), - // ie: this response was from a partitioned queue so the size is the original max size * # of partitions - EnablePartitioning: to.BoolPtr(true), - MaxSizeInMegabytes: to.Int32Ptr(16 * 4096), - RequiresDuplicateDetection: to.BoolPtr(true), - RequiresSession: to.BoolPtr(true), - DefaultMessageTimeToLive: toDurationPtr(time.Hour * 6), - DeadLetteringOnMessageExpiration: to.BoolPtr(true), - DuplicateDetectionHistoryTimeWindow: toDurationPtr(4 * time.Hour), - MaxDeliveryCount: to.Int32Ptr(100), - EnableBatchedOperations: to.BoolPtr(false), - Status: &es, - AutoDeleteOnIdle: toDurationPtr(10 * time.Minute), - }, createResp.Value) - - getResp, err := adminClient.GetQueue(context.Background(), queueName) - require.NoError(t, err) - - require.EqualValues(t, getResp.Value, createResp.Value) -} - func TestAdminClient_Queue_Forwarding(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) + cs := test.GetConnectionString(t) + adminClient, err := admin.NewClientFromConnectionString(cs, nil) require.NoError(t, err) queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) forwardToQueueName := fmt.Sprintf("queue-fwd-%X", time.Now().UnixNano()) - _, err = adminClient.AddQueue(context.Background(), forwardToQueueName) + _, err = adminClient.CreateQueue(context.Background(), forwardToQueueName, nil, nil) require.NoError(t, err) defer func() { - _, err := adminClient.DeleteQueue(context.Background(), forwardToQueueName) + _, err := adminClient.DeleteQueue(context.Background(), forwardToQueueName, nil) require.NoError(t, err) }() - formatted := fmt.Sprintf("%s%s", adminClient.em.Host, forwardToQueueName) + parsed, err := conn.ParsedConnectionFromStr(cs) + require.NoError(t, err) - createResp, err := adminClient.AddQueueWithProperties(context.Background(), &QueueProperties{ - Name: queueName, + formatted := fmt.Sprintf("%s%s", fmt.Sprintf("https://%s.%s/", parsed.Namespace, parsed.Suffix), forwardToQueueName) + + createResp, err := adminClient.CreateQueue(context.Background(), queueName, &admin.QueueProperties{ ForwardTo: &formatted, ForwardDeadLetteredMessagesTo: &formatted, - }) + }, nil) require.NoError(t, err) defer func() { - _, err := adminClient.DeleteQueue(context.Background(), queueName) + _, err := adminClient.DeleteQueue(context.Background(), queueName, nil) require.NoError(t, err) }() - require.EqualValues(t, formatted, *createResp.Value.ForwardTo) - require.EqualValues(t, formatted, *createResp.Value.ForwardDeadLetteredMessagesTo) + require.EqualValues(t, formatted, *createResp.ForwardTo) + require.EqualValues(t, formatted, *createResp.ForwardDeadLetteredMessagesTo) getResp, err := adminClient.GetQueue(context.Background(), queueName) require.NoError(t, err) - require.EqualValues(t, createResp.Value, getResp.Value) + require.EqualValues(t, createResp.QueueProperties, getResp.QueueProperties) - client, err := NewClientFromConnectionString(getConnectionString(t), nil) + client, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) require.NoError(t, err) - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) err = sender.SendMessage(context.Background(), &Message{ @@ -211,56 +72,26 @@ func TestAdminClient_Queue_Forwarding(t *testing.T) { forwardedMessage, err := receiver.receiveMessage(context.Background(), nil) require.NoError(t, err) - require.EqualValues(t, "this message will be auto-forwarded", string(forwardedMessage.Body)) -} - -func TestAdminClient_UpdateQueue(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) - createdProps, err := adminClient.AddQueue(context.Background(), queueName) + body, err := forwardedMessage.Body() require.NoError(t, err) - - defer func() { - _, err := adminClient.DeleteQueue(context.Background(), queueName) - require.NoError(t, err) - }() - - createdProps.Value.MaxDeliveryCount = to.Int32Ptr(101) - updatedProps, err := adminClient.UpdateQueue(context.Background(), createdProps.Value) - require.NoError(t, err) - - require.EqualValues(t, 101, *updatedProps.Value.MaxDeliveryCount) - - // try changing a value that's not allowed - updatedProps.Value.RequiresSession = to.BoolPtr(true) - updatedProps, err = adminClient.UpdateQueue(context.Background(), updatedProps.Value) - require.Contains(t, err.Error(), "The value for the RequiresSession property of an existing Queue cannot be changed") - require.Nil(t, updatedProps) - - createdProps.Value.Name = "non-existent-queue" - updatedProps, err = adminClient.UpdateQueue(context.Background(), createdProps.Value) - // a little awkward, we'll make these programatically inspectable as we add in better error handling. - require.Contains(t, err.Error(), "error code: 404") - require.Nil(t, updatedProps) + require.EqualValues(t, "this message will be auto-forwarded", string(body)) } func TestAdminClient_GetQueueRuntimeProperties(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) + adminClient, err := admin.NewClientFromConnectionString(test.GetConnectionString(t), nil) require.NoError(t, err) - client, err := NewClientFromConnectionString(getConnectionString(t), nil) + client, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) require.NoError(t, err) defer client.Close(context.Background()) queueName := fmt.Sprintf("queue-%X", time.Now().UnixNano()) - _, err = adminClient.AddQueue(context.Background(), queueName) + _, err = adminClient.CreateQueue(context.Background(), queueName, nil, nil) require.NoError(t, err) defer deleteQueue(t, adminClient, queueName) - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) for i := 0; i < 3; i++ { @@ -270,8 +101,8 @@ func TestAdminClient_GetQueueRuntimeProperties(t *testing.T) { require.NoError(t, err) } - sequenceNumbers, err := sender.ScheduleMessages(context.Background(), []SendableMessage{ - &Message{Body: []byte("hello")}, + sequenceNumbers, err := sender.ScheduleMessages(context.Background(), []*Message{ + {Body: []byte("hello")}, }, time.Now().Add(2*time.Hour)) require.NoError(t, err) require.NotEmpty(t, sequenceNumbers) @@ -287,847 +118,70 @@ func TestAdminClient_GetQueueRuntimeProperties(t *testing.T) { props, err := adminClient.GetQueueRuntimeProperties(context.Background(), queueName) require.NoError(t, err) - require.EqualValues(t, queueName, props.Value.Name) - - require.EqualValues(t, 4, props.Value.TotalMessageCount) - - require.EqualValues(t, 2, props.Value.ActiveMessageCount) - require.EqualValues(t, 1, props.Value.DeadLetterMessageCount) - require.EqualValues(t, 1, props.Value.ScheduledMessageCount) - require.EqualValues(t, 0, props.Value.TransferDeadLetterMessageCount) - require.EqualValues(t, 0, props.Value.TransferMessageCount) - - require.Greater(t, props.Value.SizeInBytes, int64(0)) - - require.NotEqual(t, time.Time{}, props.Value.CreatedAt) - require.NotEqual(t, time.Time{}, props.Value.UpdatedAt) - require.NotEqual(t, time.Time{}, props.Value.AccessedAt) -} - -func TestAdminClient_ListQueues(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - var expectedQueues []string - now := time.Now().UnixNano() - - for i := 0; i < 3; i++ { - queueName := strings.ToLower(fmt.Sprintf("queue-%d-%X", i, now)) - expectedQueues = append(expectedQueues, queueName) - - _, err = adminClient.AddQueueWithProperties(context.Background(), &QueueProperties{ - Name: queueName, - MaxDeliveryCount: to.Int32Ptr(int32(i + 10)), - }) - require.NoError(t, err) - - defer deleteQueue(t, adminClient, queueName) - } - - // we skipped the first queue so it shouldn't come back in the results. - pager := adminClient.ListQueues(nil) - all := map[string]*QueueProperties{} - var allNames []string - - for pager.NextPage(context.Background()) { - page := pager.PageResponse() - - for _, props := range page.Value { - _, exists := all[props.Name] - require.False(t, exists, "Each queue result should be unique") - all[props.Name] = props - allNames = append(allNames, props.Name) - } - } - - require.NoError(t, pager.Err()) - - // sanity check - the queues we created exist and their deserialization is - // working. - for i, expectedQueue := range expectedQueues { - props, exists := all[expectedQueue] - require.True(t, exists) - require.EqualValues(t, i+10, *props.MaxDeliveryCount) - } - - // grab the second to last item - pager = adminClient.ListQueues(&ListQueuesOptions{ - Skip: len(allNames) - 2, - Top: 1, - }) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, 1, len(pager.PageResponse().Value)) - require.EqualValues(t, allNames[len(allNames)-2], pager.PageResponse().Value[0].Name) - require.NoError(t, pager.Err()) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, allNames[len(allNames)-1], pager.PageResponse().Value[0].Name) - require.False(t, pager.NextPage(context.Background())) -} - -func TestAdminClient_ListQueuesRuntimeProperties(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - var expectedQueues []string - now := time.Now().UnixNano() - - for i := 0; i < 3; i++ { - queueName := strings.ToLower(fmt.Sprintf("queue-%d-%X", i, now)) - expectedQueues = append(expectedQueues, queueName) - - _, err = adminClient.AddQueueWithProperties(context.Background(), &QueueProperties{ - Name: queueName, - MaxDeliveryCount: to.Int32Ptr(int32(i + 10)), - }) - require.NoError(t, err) - - defer deleteQueue(t, adminClient, queueName) - } - - // we skipped the first queue so it shouldn't come back in the results. - pager := adminClient.ListQueuesRuntimeProperties(nil) - all := map[string]*QueueRuntimeProperties{} - var allNames []string - - for pager.NextPage(context.Background()) { - page := pager.PageResponse() - - for _, props := range page.Value { - _, exists := all[props.Name] - require.False(t, exists, "Each queue result should be unique") - all[props.Name] = props - allNames = append(allNames, props.Name) - } - } - - require.NoError(t, pager.Err()) - - // sanity check - the queues we created exist and their deserialization is - // working. - for _, expectedQueue := range expectedQueues { - props, exists := all[expectedQueue] - require.True(t, exists) - require.NotEqualValues(t, time.Time{}, props.CreatedAt) - } - - // grab the second to last item - pager = adminClient.ListQueuesRuntimeProperties(&ListQueuesRuntimePropertiesOptions{ - Skip: len(allNames) - 2, - Top: 1, - }) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, 1, len(pager.PageResponse().Value)) - require.EqualValues(t, allNames[len(allNames)-2], pager.PageResponse().Value[0].Name) - require.NoError(t, pager.Err()) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, allNames[len(allNames)-1], pager.PageResponse().Value[0].Name) - require.False(t, pager.NextPage(context.Background())) -} - -func TestAdminClient_DurationToStringPtr(t *testing.T) { - // The actual value max is TimeSpan.Max so we just assume that's what the user wants if they specify our time.Duration value - require.EqualValues(t, "P10675199DT2H48M5.4775807S", utils.DurationTo8601Seconds(utils.MaxTimeDuration), "Max time.Duration gets converted to TimeSpan.Max") - - require.EqualValues(t, "PT0M1S", utils.DurationTo8601Seconds(time.Second)) - require.EqualValues(t, "PT1M0S", utils.DurationTo8601Seconds(time.Minute)) - require.EqualValues(t, "PT1M1S", utils.DurationTo8601Seconds(time.Minute+time.Second)) - require.EqualValues(t, "PT60M0S", utils.DurationTo8601Seconds(time.Hour)) - require.EqualValues(t, "PT61M1S", utils.DurationTo8601Seconds(time.Hour+time.Minute+time.Second)) -} - -func TestAdminClient_ISO8601StringToDuration(t *testing.T) { - str := "PT10M1S" - duration, err := utils.ISO8601StringToDuration(&str) - require.NoError(t, err) - require.EqualValues(t, (10*time.Minute)+time.Second, *duration) - - duration, err = utils.ISO8601StringToDuration(nil) - require.NoError(t, err) - require.Nil(t, duration) - - str = "PT1S" - duration, err = utils.ISO8601StringToDuration(&str) - require.NoError(t, err) - require.EqualValues(t, time.Second, *duration) - - str = "PT1M" - duration, err = utils.ISO8601StringToDuration(&str) - require.NoError(t, err) - require.EqualValues(t, time.Minute, *duration) - - // this is the .NET timespan max - str = "P10675199DT2H48M5.4775807S" - duration, err = utils.ISO8601StringToDuration(&str) - require.NoError(t, err) - require.EqualValues(t, utils.MaxTimeDuration, *duration) - - // this is the Java equivalent - str = "PT256204778H48M5.4775807S" - duration, err = utils.ISO8601StringToDuration(&str) - require.NoError(t, err) - require.EqualValues(t, utils.MaxTimeDuration, *duration) -} - -func TestAdminClient_TopicAndSubscription(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - topicName := fmt.Sprintf("topic-%X", time.Now().UnixNano()) - subscriptionName := fmt.Sprintf("sub-%X", time.Now().UnixNano()) - forwardToQueueName := fmt.Sprintf("queue-fwd-%X", time.Now().UnixNano()) - - _, err = adminClient.AddQueue(context.Background(), forwardToQueueName) - require.NoError(t, err) - - defer deleteQueue(t, adminClient, forwardToQueueName) - - status := EntityStatusActive - - // check topic properties, existence - topicExists, err := adminClient.TopicExists(context.Background(), topicName) - require.False(t, topicExists) - require.NoError(t, err) - - addResp, err := adminClient.AddTopicWithProperties(context.Background(), &TopicProperties{ - Name: topicName, - EnablePartitioning: to.BoolPtr(true), - MaxSizeInMegabytes: to.Int32Ptr(2048), - RequiresDuplicateDetection: to.BoolPtr(true), - DefaultMessageTimeToLive: toDurationPtr(time.Minute * 3), - DuplicateDetectionHistoryTimeWindow: toDurationPtr(time.Minute * 4), - EnableBatchedOperations: to.BoolPtr(true), - Status: &status, - AutoDeleteOnIdle: toDurationPtr(time.Minute * 7), - SupportsOrdering: to.BoolPtr(true), - }) - require.NoError(t, err) + require.EqualValues(t, 4, props.TotalMessageCount) - defer deleteTopic(t, adminClient, topicName) + require.EqualValues(t, 2, props.ActiveMessageCount) + require.EqualValues(t, 1, props.DeadLetterMessageCount) + require.EqualValues(t, 1, props.ScheduledMessageCount) + require.EqualValues(t, 0, props.TransferDeadLetterMessageCount) + require.EqualValues(t, 0, props.TransferMessageCount) - topicExists, err = adminClient.TopicExists(context.Background(), topicName) - require.True(t, topicExists) - require.NoError(t, err) + require.Greater(t, props.SizeInBytes, int64(0)) - require.EqualValues(t, &TopicProperties{ - Name: topicName, - EnablePartitioning: to.BoolPtr(true), - MaxSizeInMegabytes: to.Int32Ptr(16 * 2048), // enabling partitioning increases our max size because of the 16 partitions - RequiresDuplicateDetection: to.BoolPtr(true), - DefaultMessageTimeToLive: toDurationPtr(time.Minute * 3), - DuplicateDetectionHistoryTimeWindow: toDurationPtr(time.Minute * 4), - EnableBatchedOperations: to.BoolPtr(true), - Status: &status, - AutoDeleteOnIdle: toDurationPtr(time.Minute * 7), - SupportsOrdering: to.BoolPtr(true), - }, addResp.Value) - - getResp, err := adminClient.GetTopic(context.Background(), topicName) - require.NoError(t, err) - - require.EqualValues(t, &TopicProperties{ - Name: topicName, - EnablePartitioning: to.BoolPtr(true), - MaxSizeInMegabytes: to.Int32Ptr(16 * 2048), // enabling partitioning increases our max size because of the 16 partitions - RequiresDuplicateDetection: to.BoolPtr(true), - DefaultMessageTimeToLive: toDurationPtr(time.Minute * 3), - DuplicateDetectionHistoryTimeWindow: toDurationPtr(time.Minute * 4), - EnableBatchedOperations: to.BoolPtr(true), - Status: &status, - AutoDeleteOnIdle: toDurationPtr(time.Minute * 7), - SupportsOrdering: to.BoolPtr(true), - }, getResp.Value) - - // check some subscriptions properties, existence - subExists, err := adminClient.SubscriptionExists(context.Background(), topicName, subscriptionName) - require.NoError(t, err) - require.False(t, subExists) - - addSubWithPropsResp, err := adminClient.AddSubscriptionWithProperties(context.Background(), topicName, &SubscriptionProperties{ - Name: subscriptionName, - LockDuration: toDurationPtr(3 * time.Minute), - RequiresSession: to.BoolPtr(false), - DefaultMessageTimeToLive: toDurationPtr(7 * time.Minute), - DeadLetteringOnMessageExpiration: to.BoolPtr(true), - EnableDeadLetteringOnFilterEvaluationExceptions: to.BoolPtr(false), - MaxDeliveryCount: to.Int32Ptr(11), - Status: &status, - // ForwardTo: &forwardToQueueName, - // ForwardDeadLetteredMessagesTo: &forwardToQueueName, - EnableBatchedOperations: to.BoolPtr(false), - UserMetadata: to.StringPtr("some user metadata"), - }) - - require.NoError(t, err) - require.EqualValues(t, &SubscriptionProperties{ - Name: subscriptionName, - LockDuration: toDurationPtr(3 * time.Minute), - RequiresSession: to.BoolPtr(false), - DefaultMessageTimeToLive: toDurationPtr(7 * time.Minute), - DeadLetteringOnMessageExpiration: to.BoolPtr(true), - EnableDeadLetteringOnFilterEvaluationExceptions: to.BoolPtr(false), - MaxDeliveryCount: to.Int32Ptr(11), - Status: &status, - // ForwardTo: &forwardToQueueName, - // ForwardDeadLetteredMessagesTo: &forwardToQueueName, - EnableBatchedOperations: to.BoolPtr(false), - UserMetadata: to.StringPtr("some user metadata"), - }, addSubWithPropsResp.Value) - - subExists, err = adminClient.SubscriptionExists(context.Background(), topicName, subscriptionName) - require.NoError(t, err) - require.True(t, subExists) - - defer deleteSubscription(t, adminClient, topicName, subscriptionName) -} - -func TestAdminClient_UpdateTopic(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - topicName := fmt.Sprintf("topic-%X", time.Now().UnixNano()) - addResp, err := adminClient.AddTopic(context.Background(), topicName) - require.NoError(t, err) - - defer deleteTopic(t, adminClient, topicName) - - addResp.Value.AutoDeleteOnIdle = toDurationPtr(11 * time.Minute) - updateResp, err := adminClient.UpdateTopic(context.Background(), addResp.Value) - require.NoError(t, err) - - require.EqualValues(t, 11*time.Minute, *updateResp.Value.AutoDeleteOnIdle) - - // try changing a value that's not allowed - updateResp.Value.EnablePartitioning = to.BoolPtr(true) - updateResp, err = adminClient.UpdateTopic(context.Background(), updateResp.Value) - require.Contains(t, err.Error(), "Partitioning cannot be changed for Topic. ") - require.Nil(t, updateResp) - - addResp.Value.Name = "non-existent-topic" - updateResp, err = adminClient.UpdateTopic(context.Background(), addResp.Value) - // a little awkward, we'll make these programatically inspectable as we add in better error handling. - require.Contains(t, err.Error(), "error code: 404") - require.Nil(t, updateResp) + require.NotEqual(t, time.Time{}, props.CreatedAt) + require.NotEqual(t, time.Time{}, props.UpdatedAt) + require.NotEqual(t, time.Time{}, props.AccessedAt) } func TestAdminClient_TopicAndSubscriptionRuntimeProperties(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) + adminClient, err := admin.NewClientFromConnectionString(test.GetConnectionString(t), nil) require.NoError(t, err) - client, err := NewClientFromConnectionString(getConnectionString(t), nil) + client, err := NewClientFromConnectionString(test.GetConnectionString(t), nil) require.NoError(t, err) topicName := fmt.Sprintf("topic-%X", time.Now().UnixNano()) subscriptionName := fmt.Sprintf("sub-%X", time.Now().UnixNano()) - _, err = adminClient.AddTopic(context.Background(), topicName) + _, err = adminClient.CreateTopic(context.Background(), topicName, nil, nil) require.NoError(t, err) - addSubResp, err := adminClient.AddSubscription(context.Background(), topicName, subscriptionName) + addSubResp, err := adminClient.CreateSubscription(context.Background(), topicName, subscriptionName, nil, nil) require.NoError(t, err) require.NotNil(t, addSubResp) - require.EqualValues(t, subscriptionName, addSubResp.Value.Name) + require.EqualValues(t, 10, *addSubResp.MaxDeliveryCount) defer deleteSubscription(t, adminClient, topicName, subscriptionName) - sender, err := client.NewSender(topicName) + sender, err := client.NewSender(topicName, nil) require.NoError(t, err) // trigger some stats // Scheduled messages are accounted for in the topic stats. - _, err = sender.ScheduleMessages(context.Background(), []SendableMessage{ - &Message{Body: []byte("hello")}, + _, err = sender.ScheduleMessages(context.Background(), []*Message{ + {Body: []byte("hello")}, }, time.Now().Add(2*time.Hour)) require.NoError(t, err) // validate the topic runtime properties - getRuntimeResp, err := adminClient.GetTopicRuntimeProperties(context.Background(), topicName) + getRuntimeResp, err := adminClient.GetTopicRuntimeProperties(context.Background(), topicName, nil) require.NoError(t, err) - require.EqualValues(t, topicName, getRuntimeResp.Value.Name) - - require.EqualValues(t, 1, getRuntimeResp.Value.SubscriptionCount) - require.NotEqual(t, time.Time{}, getRuntimeResp.Value.CreatedAt) - require.NotEqual(t, time.Time{}, getRuntimeResp.Value.UpdatedAt) - require.NotEqual(t, time.Time{}, getRuntimeResp.Value.AccessedAt) + require.EqualValues(t, 1, getRuntimeResp.SubscriptionCount) + require.NotEqual(t, time.Time{}, getRuntimeResp.CreatedAt) + require.NotEqual(t, time.Time{}, getRuntimeResp.UpdatedAt) + require.NotEqual(t, time.Time{}, getRuntimeResp.AccessedAt) - require.Greater(t, getRuntimeResp.Value.SizeInBytes, int64(0)) - require.EqualValues(t, int32(1), getRuntimeResp.Value.ScheduledMessageCount) + require.Greater(t, getRuntimeResp.SizeInBytes, int64(0)) + require.EqualValues(t, int32(1), getRuntimeResp.ScheduledMessageCount) // validate subscription runtime properties getSubResp, err := adminClient.GetSubscriptionRuntimeProperties(context.Background(), topicName, subscriptionName) require.NoError(t, err) - require.EqualValues(t, subscriptionName, getSubResp.Value.Name) - require.EqualValues(t, 0, getSubResp.Value.ActiveMessageCount) - require.NotEqual(t, time.Time{}, getSubResp.Value.CreatedAt) - require.NotEqual(t, time.Time{}, getSubResp.Value.UpdatedAt) - require.NotEqual(t, time.Time{}, getSubResp.Value.AccessedAt) -} - -func TestAdminClient_ListTopics(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - var expectedTopics []string - now := time.Now().UnixNano() - - wg := sync.WaitGroup{} - - for i := 0; i < 3; i++ { - topicName := strings.ToLower(fmt.Sprintf("topic-%d-%X", i, now)) - expectedTopics = append(expectedTopics, topicName) - wg.Add(1) - - go func(i int) { - defer wg.Done() - _, err = adminClient.AddTopicWithProperties(context.Background(), &TopicProperties{ - Name: topicName, - DefaultMessageTimeToLive: toDurationPtr(time.Duration(i+1) * time.Minute), - }) - require.NoError(t, err) - }(i) - - defer deleteTopic(t, adminClient, topicName) - } - - wg.Wait() - - // we skipped the first topic so it shouldn't come back in the results. - pager := adminClient.ListTopics(nil) - all := map[string]*TopicProperties{} - var allNames []string - - for pager.NextPage(context.Background()) { - page := pager.PageResponse() - - for _, props := range page.Value { - _, exists := all[props.Name] - require.False(t, exists, "Each topic result should be unique") - all[props.Name] = props - allNames = append(allNames, props.Name) - } - } - - require.NoError(t, pager.Err()) - - // sanity check - the topics we created exist and their deserialization is - // working. - for i, expectedTopic := range expectedTopics { - props, exists := all[expectedTopic] - require.True(t, exists) - require.EqualValues(t, time.Duration(i+1)*time.Minute, *props.DefaultMessageTimeToLive) - } - - // grab the second to last item - pager = adminClient.ListTopics(&ListTopicsOptions{ - Skip: len(allNames) - 2, - Top: 1, - }) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, 1, len(pager.PageResponse().Value)) - require.EqualValues(t, allNames[len(allNames)-2], pager.PageResponse().Value[0].Name) - require.NoError(t, pager.Err()) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, allNames[len(allNames)-1], pager.PageResponse().Value[0].Name) - require.False(t, pager.NextPage(context.Background())) -} - -func TestAdminClient_ListTopicsRuntimeProperties(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - var expectedTopics []string - now := time.Now().UnixNano() - - for i := 0; i < 3; i++ { - topicName := strings.ToLower(fmt.Sprintf("topic-%d-%X", i, now)) - expectedTopics = append(expectedTopics, topicName) - - _, err = adminClient.AddTopic(context.Background(), topicName) - require.NoError(t, err) - - defer deleteTopic(t, adminClient, topicName) - } - - // we skipped the first topic so it shouldn't come back in the results. - pager := adminClient.ListTopicsRuntimeProperties(nil) - all := map[string]*TopicRuntimeProperties{} - var allNames []string - - for pager.NextPage(context.Background()) { - page := pager.PageResponse() - - for _, props := range page.Value { - _, exists := all[props.Name] - require.False(t, exists, "Each topic result should be unique") - all[props.Name] = props - allNames = append(allNames, props.Name) - } - } - - require.NoError(t, pager.Err()) - - // sanity check - the topics we created exist and their deserialization is - // working. - for _, expectedTopic := range expectedTopics { - props, exists := all[expectedTopic] - require.True(t, exists) - require.NotEqualValues(t, time.Time{}, props.CreatedAt) - } - - // grab the second to last item - pager = adminClient.ListTopicsRuntimeProperties(&ListTopicsRuntimePropertiesOptions{ - Skip: len(allNames) - 2, - Top: 1, - }) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, 1, len(pager.PageResponse().Value)) - require.EqualValues(t, allNames[len(allNames)-2], pager.PageResponse().Value[0].Name) - require.NoError(t, pager.Err()) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, allNames[len(allNames)-1], pager.PageResponse().Value[0].Name) - require.False(t, pager.NextPage(context.Background())) -} - -func TestAdminClient_ListSubscriptions(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - now := time.Now().UnixNano() - topicName := strings.ToLower(fmt.Sprintf("topic-%X", now)) - - _, err = adminClient.AddTopic(context.Background(), topicName) - require.NoError(t, err) - - defer deleteTopic(t, adminClient, topicName) - - var expectedSubscriptions []string - - for i := 0; i < 3; i++ { - subName := strings.ToLower(fmt.Sprintf("sub-%d-%X", i, now)) - expectedSubscriptions = append(expectedSubscriptions, subName) - - _, err = adminClient.AddSubscriptionWithProperties(context.Background(), topicName, &SubscriptionProperties{ - Name: subName, - DefaultMessageTimeToLive: toDurationPtr(time.Duration(i+1) * time.Minute), - }) - require.NoError(t, err) - - defer deleteSubscription(t, adminClient, topicName, subName) - } - - // we skipped the first topic so it shouldn't come back in the results. - pager := adminClient.ListSubscriptions(topicName, nil) - all := map[string]*SubscriptionProperties{} - var allNames []string - - for pager.NextPage(context.Background()) { - page := pager.PageResponse() - - for _, props := range page.Value { - _, exists := all[props.Name] - require.False(t, exists, "Each subscription result should be unique") - all[props.Name] = props - allNames = append(allNames, props.Name) - } - } - - require.NoError(t, pager.Err()) - - // sanity check - the subscriptions we created exist and their deserialization is - // working. - for i, expectedTopic := range expectedSubscriptions { - props, exists := all[expectedTopic] - require.True(t, exists) - require.EqualValues(t, time.Duration(i+1)*time.Minute, *props.DefaultMessageTimeToLive) - } - - // grab the second to last item - pager = adminClient.ListSubscriptions(topicName, &ListSubscriptionsOptions{ - Skip: len(allNames) - 2, - Top: 1, - }) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, 1, len(pager.PageResponse().Value)) - require.EqualValues(t, allNames[len(allNames)-2], pager.PageResponse().Value[0].Name) - require.NoError(t, pager.Err()) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, allNames[len(allNames)-1], pager.PageResponse().Value[0].Name) - require.False(t, pager.NextPage(context.Background())) -} - -func TestAdminClient_ListSubscriptionRuntimeProperties(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - now := time.Now().UnixNano() - topicName := strings.ToLower(fmt.Sprintf("topic-%X", now)) - - _, err = adminClient.AddTopic(context.Background(), topicName) - require.NoError(t, err) - - var expectedSubs []string - - for i := 0; i < 3; i++ { - subscriptionName := strings.ToLower(fmt.Sprintf("sub-%d-%X", i, now)) - expectedSubs = append(expectedSubs, subscriptionName) - - _, err = adminClient.AddSubscription(context.Background(), topicName, subscriptionName) - require.NoError(t, err) - - defer deleteSubscription(t, adminClient, topicName, subscriptionName) - } - - // we skipped the first subscription so it shouldn't come back in the results. - pager := adminClient.ListSubscriptionsRuntimeProperties(topicName, nil) - all := map[string]*SubscriptionRuntimeProperties{} - var allNames []string - - for pager.NextPage(context.Background()) { - page := pager.PageResponse() - - for _, props := range page.Value { - _, exists := all[props.Name] - require.False(t, exists, "Each subscription result should be unique") - all[props.Name] = props - allNames = append(allNames, props.Name) - } - } - - require.NoError(t, pager.Err()) - - // sanity check - the topics we created exist and their deserialization is - // working. - for _, expectedSub := range expectedSubs { - props, exists := all[expectedSub] - require.True(t, exists) - require.NotEqualValues(t, time.Time{}, props.CreatedAt) - } - - // grab the second to last item - pager = adminClient.ListSubscriptionsRuntimeProperties(topicName, &ListSubscriptionsRuntimePropertiesOptions{ - Skip: len(allNames) - 2, - Top: 1, - }) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, 1, len(pager.PageResponse().Value)) - require.EqualValues(t, allNames[len(allNames)-2], pager.PageResponse().Value[0].Name) - require.NoError(t, pager.Err()) - - require.True(t, pager.NextPage(context.Background())) - require.EqualValues(t, allNames[len(allNames)-1], pager.PageResponse().Value[0].Name) - require.False(t, pager.NextPage(context.Background())) -} - -func TestAdminClient_UpdateSubscription(t *testing.T) { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - topicName := fmt.Sprintf("topic-%X", time.Now().UnixNano()) - _, err = adminClient.AddTopic(context.Background(), topicName) - require.NoError(t, err) - - defer deleteTopic(t, adminClient, topicName) - - subscriptionName := fmt.Sprintf("sub-%X", time.Now().UnixNano()) - addResp, err := adminClient.AddSubscription(context.Background(), topicName, subscriptionName) - require.NoError(t, err) - - defer deleteSubscription(t, adminClient, topicName, subscriptionName) - - addResp.Value.LockDuration = toDurationPtr(4 * time.Minute) - updateResp, err := adminClient.UpdateSubscription(context.Background(), topicName, addResp.Value) - require.NoError(t, err) - - require.EqualValues(t, 4*time.Minute, *updateResp.Value.LockDuration) - - // try changing a value that's not allowed - updateResp.Value.RequiresSession = to.BoolPtr(true) - updateResp, err = adminClient.UpdateSubscription(context.Background(), topicName, updateResp.Value) - require.Contains(t, err.Error(), "The value for the RequiresSession property of an existing Subscription cannot be changed") - require.Nil(t, updateResp) - - addResp.Value.Name = "non-existent-subscription" - updateResp, err = adminClient.UpdateSubscription(context.Background(), topicName, addResp.Value) - // a little awkward, we'll make these programatically inspectable as we add in better error handling. - require.Contains(t, err.Error(), "error code: 404") - require.Nil(t, updateResp) -} - -func setupLowPrivTest(t *testing.T) *struct { - Client *AdminClient - TopicName string - SubName string - QueueName string - Cleanup func() -} { - adminClient, err := NewAdminClientWithConnectionString(getConnectionString(t), nil) - require.NoError(t, err) - - lowPrivAdminClient, err := NewAdminClientWithConnectionString(getConnectionStringWithoutManagePerms(t), nil) - require.NoError(t, err) - - nanoSeconds := time.Now().UnixNano() - - topicName := fmt.Sprintf("topic-%d", nanoSeconds) - queueName := fmt.Sprintf("queue-%d", nanoSeconds) - subName := "subscription1" - - // TODO: add in rule management - //ruleName := "rule" - - // create some entities that we need (there's a diff between something not being - // found and something failing because of lack of authorization) - cleanup := func() func() { - _, err = adminClient.AddQueue(context.Background(), queueName) - require.NoError(t, err) - - _, err = adminClient.AddTopic(context.Background(), topicName) - require.NoError(t, err) - - _, err = adminClient.AddSubscription(context.Background(), topicName, subName) - require.NoError(t, err) - - // _, err = sm.PutRule(context.Background(), subName, ruleName, TrueFilter{}) - // require.NoError(t, err) - - return func() { - deleteTopic(t, adminClient, topicName) // will also delete the subscription - deleteQueue(t, adminClient, queueName) - } - }() - - return &struct { - Client *AdminClient - TopicName string - SubName string - QueueName string - Cleanup func() - }{ - Client: lowPrivAdminClient, - QueueName: queueName, - TopicName: topicName, - SubName: subName, - Cleanup: cleanup, - } -} - -func TestAdminClient_LackPermissions_Queue(t *testing.T) { - testData := setupLowPrivTest(t) - defer testData.Cleanup() - - ctx := context.Background() - - _, err := testData.Client.GetQueue(ctx, "not-found-queue") - require.True(t, atom.NotFound(err)) - - _, err = testData.Client.GetQueue(ctx, testData.QueueName) - require.Contains(t, err.Error(), "error code: 401, Details: Manage,EntityRead claims") - - pager := testData.Client.ListQueues(nil) - require.False(t, pager.NextPage(context.Background())) - require.Contains(t, pager.Err().Error(), "error code: 401, Details: Manage,EntityRead claims required for this operation") - - _, err = testData.Client.AddQueue(ctx, "canneverbecreated") - require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityWrite") - - _, err = testData.Client.UpdateQueue(ctx, &QueueProperties{ - Name: "canneverbecreated", - }) - require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityWrite") - - _, err = testData.Client.DeleteQueue(ctx, testData.QueueName) - require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityDelete.") -} - -func TestAdminClient_LackPermissions_Topic(t *testing.T) { - testData := setupLowPrivTest(t) - defer testData.Cleanup() - - ctx := context.Background() - - _, err := testData.Client.GetTopic(ctx, "not-found-topic") - require.True(t, atom.NotFound(err)) - - _, err = testData.Client.GetTopic(ctx, testData.TopicName) - require.Contains(t, err.Error(), "error code: 401, Details: Manage,EntityRead claims") - - pager := testData.Client.ListTopics(nil) - require.False(t, pager.NextPage(context.Background())) - require.Contains(t, pager.Err().Error(), "error code: 401, Details: Manage,EntityRead claims required for this operation") - - _, err = testData.Client.AddTopic(ctx, "canneverbecreated") - require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action") - - _, err = testData.Client.UpdateTopic(ctx, &TopicProperties{ - Name: "canneverbecreated", - }) - require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action") - - _, err = testData.Client.DeleteTopic(ctx, testData.TopicName) - require.Contains(t, err.Error(), "error code: 401, Details: Authorization failed for specified action: Manage,EntityDelete.") - - // sanity check that the http response is getting bundled into these errors, should it be needed. - var httpResponse azcore.HTTPResponse - require.True(t, errors.As(err, &httpResponse)) - require.EqualValues(t, http.StatusUnauthorized, httpResponse.RawResponse().StatusCode) -} - -func TestAdminClient_LackPermissions_Subscription(t *testing.T) { - testData := setupLowPrivTest(t) - defer testData.Cleanup() - - ctx := context.Background() - - _, err := testData.Client.GetSubscription(ctx, testData.TopicName, "not-found-sub") - require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'GetSubscription'") - - _, err = testData.Client.GetSubscription(ctx, testData.TopicName, testData.SubName) - require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'GetSubscription'") - - pager := testData.Client.ListSubscriptions(testData.TopicName, nil) - require.False(t, pager.NextPage(context.Background())) - require.Contains(t, pager.Err().Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'EnumerateSubscriptions' operation") - - _, err = testData.Client.AddSubscription(ctx, testData.TopicName, "canneverbecreated") - require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'CreateOrUpdateSubscription'") - - _, err = testData.Client.UpdateSubscription(ctx, testData.TopicName, &SubscriptionProperties{ - Name: "canneverbecreated", - }) - require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'CreateOrUpdateSubscription'") - - _, err = testData.Client.DeleteSubscription(ctx, testData.TopicName, testData.SubName) - require.Contains(t, err.Error(), "401 SubCode=40100: Unauthorized : Unauthorized access for 'DeleteSubscription'") -} - -func toDurationPtr(d time.Duration) *time.Duration { - return &d -} - -func deleteQueue(t *testing.T, ac *AdminClient, queueName string) { - _, err := ac.DeleteQueue(context.Background(), queueName) - require.NoError(t, err) -} - -func deleteTopic(t *testing.T, ac *AdminClient, topicName string) { - _, err := ac.DeleteTopic(context.Background(), topicName) - require.NoError(t, err) -} - -func deleteSubscription(t *testing.T, ac *AdminClient, topicName string, subscriptionName string) { - _, err := ac.DeleteSubscription(context.Background(), topicName, subscriptionName) - require.NoError(t, err) + require.EqualValues(t, 0, getSubResp.ActiveMessageCount) + require.NotEqual(t, time.Time{}, getSubResp.CreatedAt) + require.NotEqual(t, time.Time{}, getSubResp.UpdatedAt) + require.NotEqual(t, time.Time{}, getSubResp.AccessedAt) } diff --git a/sdk/messaging/azservicebus/client.go b/sdk/messaging/azservicebus/client.go index 82a1157bec59..607af7460cbc 100644 --- a/sdk/messaging/azservicebus/client.go +++ b/sdk/messaging/azservicebus/client.go @@ -71,6 +71,10 @@ func NewClientFromConnectionString(connectionString string, options *ClientOptio }, options) } +// Next overloads (ie, credential sticks with the client) +// func NewClientWithNamedKeyCredential(fullyQualifiedNamespace string, credential azcore.TokenCredential, options *ClientOptions) (*Client, error) { +// } + type clientConfig struct { connectionString string credential azcore.TokenCredential @@ -121,9 +125,9 @@ func newClientImpl(config clientConfig, options *ClientOptions) (*Client, error) } // NewReceiver creates a Receiver for a queue. A receiver allows you to receive messages. -func (client *Client) NewReceiverForQueue(queue string, options *ReceiverOptions) (*Receiver, error) { +func (client *Client) NewReceiverForQueue(queueName string, options *ReceiverOptions) (*Receiver, error) { id, cleanupOnClose := client.getCleanupForCloseable() - receiver, err := newReceiver(client.namespace, &entity{Queue: queue}, cleanupOnClose, options, nil) + receiver, err := newReceiver(client.namespace, &entity{Queue: queueName}, cleanupOnClose, options, nil) if err != nil { return nil, err @@ -134,9 +138,9 @@ func (client *Client) NewReceiverForQueue(queue string, options *ReceiverOptions } // NewReceiver creates a Receiver for a subscription. A receiver allows you to receive messages. -func (client *Client) NewReceiverForSubscription(topic string, subscription string, options *ReceiverOptions) (*Receiver, error) { +func (client *Client) NewReceiverForSubscription(topicName string, subscriptionName string, options *ReceiverOptions) (*Receiver, error) { id, cleanupOnClose := client.getCleanupForCloseable() - receiver, err := newReceiver(client.namespace, &entity{Topic: topic, Subscription: subscription}, cleanupOnClose, options, nil) + receiver, err := newReceiver(client.namespace, &entity{Topic: topicName, Subscription: subscriptionName}, cleanupOnClose, options, nil) if err != nil { return nil, err @@ -146,8 +150,12 @@ func (client *Client) NewReceiverForSubscription(topic string, subscription stri return receiver, nil } +type NewSenderOptions struct { + // For future expansion +} + // NewSender creates a Sender, which allows you to send messages or schedule messages. -func (client *Client) NewSender(queueOrTopic string) (*Sender, error) { +func (client *Client) NewSender(queueOrTopic string, options *NewSenderOptions) (*Sender, error) { id, cleanupOnClose := client.getCleanupForCloseable() sender, err := newSender(client.namespace, queueOrTopic, cleanupOnClose) @@ -161,13 +169,13 @@ func (client *Client) NewSender(queueOrTopic string) (*Sender, error) { // AcceptSessionForQueue accepts a session from a queue with a specific session ID. // NOTE: this receiver is initialized immediately, not lazily. -func (client *Client) AcceptSessionForQueue(ctx context.Context, queue string, sessionID string, options *SessionReceiverOptions) (*SessionReceiver, error) { +func (client *Client) AcceptSessionForQueue(ctx context.Context, queueName string, sessionID string, options *SessionReceiverOptions) (*SessionReceiver, error) { id, cleanupOnClose := client.getCleanupForCloseable() sessionReceiver, err := newSessionReceiver( ctx, &sessionID, client.namespace, - &entity{Queue: queue}, + &entity{Queue: queueName}, cleanupOnClose, toReceiverOptions(options)) @@ -185,13 +193,13 @@ func (client *Client) AcceptSessionForQueue(ctx context.Context, queue string, s // AcceptSessionForSubscription accepts a session from a subscription with a specific session ID. // NOTE: this receiver is initialized immediately, not lazily. -func (client *Client) AcceptSessionForSubscription(ctx context.Context, topic string, subscription string, sessionID string, options *SessionReceiverOptions) (*SessionReceiver, error) { +func (client *Client) AcceptSessionForSubscription(ctx context.Context, topicName string, subscriptionName string, sessionID string, options *SessionReceiverOptions) (*SessionReceiver, error) { id, cleanupOnClose := client.getCleanupForCloseable() sessionReceiver, err := newSessionReceiver( ctx, &sessionID, client.namespace, - &entity{Topic: topic, Subscription: subscription}, + &entity{Topic: topicName, Subscription: subscriptionName}, cleanupOnClose, toReceiverOptions(options)) @@ -209,13 +217,13 @@ func (client *Client) AcceptSessionForSubscription(ctx context.Context, topic st // AcceptNextSessionForQueue accepts the next available session from a queue. // NOTE: this receiver is initialized immediately, not lazily. -func (client *Client) AcceptNextSessionForQueue(ctx context.Context, queue string, options *SessionReceiverOptions) (*SessionReceiver, error) { +func (client *Client) AcceptNextSessionForQueue(ctx context.Context, queueName string, options *SessionReceiverOptions) (*SessionReceiver, error) { id, cleanupOnClose := client.getCleanupForCloseable() sessionReceiver, err := newSessionReceiver( ctx, nil, client.namespace, - &entity{Queue: queue}, + &entity{Queue: queueName}, cleanupOnClose, toReceiverOptions(options)) @@ -233,13 +241,13 @@ func (client *Client) AcceptNextSessionForQueue(ctx context.Context, queue strin // AcceptNextSessionForSubscription accepts the next available session from a subscription. // NOTE: this receiver is initialized immediately, not lazily. -func (client *Client) AcceptNextSessionForSubscription(ctx context.Context, topic string, subscription string, options *SessionReceiverOptions) (*SessionReceiver, error) { +func (client *Client) AcceptNextSessionForSubscription(ctx context.Context, topicName string, subscriptionName string, options *SessionReceiverOptions) (*SessionReceiver, error) { id, cleanupOnClose := client.getCleanupForCloseable() sessionReceiver, err := newSessionReceiver( ctx, nil, client.namespace, - &entity{Topic: topic, Subscription: subscription}, + &entity{Topic: topicName, Subscription: subscriptionName}, cleanupOnClose, toReceiverOptions(options)) diff --git a/sdk/messaging/azservicebus/client_test.go b/sdk/messaging/azservicebus/client_test.go index 3510a5bec19d..6ff5474db1f3 100644 --- a/sdk/messaging/azservicebus/client_test.go +++ b/sdk/messaging/azservicebus/client_test.go @@ -11,11 +11,12 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/test" "github.com/stretchr/testify/require" ) func TestNewClientWithAzureIdentity(t *testing.T) { - queue, cleanup := createQueue(t, getConnectionString(t), nil) + queue, cleanup := createQueue(t, test.GetConnectionString(t), nil) defer cleanup() // test with azure identity support @@ -29,7 +30,7 @@ func TestNewClientWithAzureIdentity(t *testing.T) { client, err := NewClient(ns, envCred, nil) require.NoError(t, err) - sender, err := client.NewSender(queue) + sender, err := client.NewSender(queue, nil) require.NoError(t, err) err = sender.SendMessage(context.TODO(), &Message{Body: []byte("hello - authenticating with a TokenCredential")}) @@ -109,7 +110,7 @@ func TestNewClientUnitTests(t *testing.T) { } client, ns := setupClient() - _, err := client.NewSender("hello") + _, err := client.NewSender("hello", nil) require.NoError(t, err) require.EqualValues(t, 1, len(client.links)) diff --git a/sdk/messaging/azservicebus/doc.go b/sdk/messaging/azservicebus/doc.go index 61d5de94452e..99941409e6d5 100644 --- a/sdk/messaging/azservicebus/doc.go +++ b/sdk/messaging/azservicebus/doc.go @@ -4,6 +4,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// Package azservicebus provides clients for sending and receiving messages with Azure ServiceBus -// as well as modifying resources like Queues, Topics and Subscriptions. +// Package azservicebus provides clients for sending and receiving messages with Azure ServiceBus. +// NOTE: for creating and managing entities, use `admin.Client` instead. package azservicebus diff --git a/sdk/messaging/azservicebus/example_admin_client_test.go b/sdk/messaging/azservicebus/example_admin_client_test.go deleted file mode 100644 index 4bf11e16ab1f..000000000000 --- a/sdk/messaging/azservicebus/example_admin_client_test.go +++ /dev/null @@ -1,79 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package azservicebus_test - -import ( - "context" - "fmt" - "time" - - "github.com/Azure/azure-sdk-for-go/sdk/azidentity" - "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" -) - -func ExampleNewAdminClient() { - // NOTE: If you'd like to authenticate using a Service Bus connection string - // look at `NewClientWithConnectionString` instead. - - credential, err := azidentity.NewDefaultAzureCredential(nil) - exitOnError("Failed to create a DefaultAzureCredential", err) - - adminClient, err = azservicebus.NewAdminClient("", credential, nil) - exitOnError("Failed to create ServiceBusClient in example", err) -} - -func ExampleNewAdminClientWithConnectionString() { - // NOTE: If you'd like to authenticate via Azure Active Directory look at - // the `NewClient` function instead. - - adminClient, err = azservicebus.NewAdminClientWithConnectionString(connectionString, nil) - exitOnError("Failed to create ServiceBusClient in example", err) -} - -func ExampleAdminClient_AddQueue() { - resp, err := adminClient.AddQueue(context.TODO(), "queue-name") - exitOnError("Failed to add queue", err) - - fmt.Printf("Queue name: %s", resp.Value.Name) -} - -func ExampleAdminClient_AddQueueWithProperties() { - lockDuration := time.Minute - maxDeliveryCount := int32(10) - - resp, err := adminClient.AddQueueWithProperties(context.TODO(), &azservicebus.QueueProperties{ - Name: "queue-name", - - // some example properties - LockDuration: &lockDuration, - MaxDeliveryCount: &maxDeliveryCount, - }) - exitOnError("Failed to create queue", err) - - fmt.Printf("Queue name: %s", resp.Value.Name) -} - -func ExampleAdminClient_ListQueues() { - queuePager := adminClient.ListQueues(nil) - - for queuePager.NextPage(context.TODO()) { - for _, queue := range queuePager.PageResponse().Value { - fmt.Printf("Queue name: %s, max size in MB: %d", queue.Name, queue.MaxSizeInMegabytes) - } - } - - exitOnError("Failed when listing queues", queuePager.Err()) -} - -func ExampleAdminClient_ListQueuesRuntimeProperties() { - queuePager := adminClient.ListQueuesRuntimeProperties(nil) - - for queuePager.NextPage(context.TODO()) { - for _, queue := range queuePager.PageResponse().Value { - fmt.Printf("Queue name: %s, active messages: %d", queue.Name, queue.ActiveMessageCount) - } - } - - exitOnError("Failed when listing queues runtime properties", queuePager.Err()) -} diff --git a/sdk/messaging/azservicebus/example_sender_test.go b/sdk/messaging/azservicebus/example_sender_test.go index 58c3f4115da0..91d9065c77f1 100644 --- a/sdk/messaging/azservicebus/example_sender_test.go +++ b/sdk/messaging/azservicebus/example_sender_test.go @@ -5,14 +5,14 @@ package azservicebus_test import ( "context" - "log" + "fmt" "time" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" ) func ExampleClient_NewSender() { - sender, err = client.NewSender("exampleQueue") // or topicName + sender, err = client.NewSender("exampleQueue", nil) // or topicName exitOnError("Failed to create sender", err) } @@ -29,31 +29,28 @@ func ExampleSender_SendMessage_messageBatch() { batch, err := sender.NewMessageBatch(context.TODO(), nil) exitOnError("Failed to create message batch", err) - messagesToSend := []*azservicebus.Message{ - {Body: []byte("hello world")}, - {Body: []byte("hello world as well")}, - } - - for i := 0; i < len(messagesToSend); i++ { - added, err := batch.Add(messagesToSend[i]) - - if added { - continue - } + // By calling AddMessage multiple times you can add multiple messages into a + // batch. This can help with message throughput, as you can send multiple + // messages in a single send. + err = batch.AddMessage(&azservicebus.Message{Body: []byte("hello world")}) - if err == nil { + if err != nil { + switch err { + case azservicebus.ErrMessageTooLarge: // At this point you can do a few things: // 1. Ignore this message // 2. Send this batch (it's full) and create a new batch. // - // The batch can still be used after this error. - log.Fatal("Failed to add message to batch (batch is full)") + // The batch can still be used after this error if you have + // smaller messages you'd still like to add in. + fmt.Printf("Failed to add message to batch\n") + default: + exitOnError("Error while trying to add message to batch", err) } - - exitOnError("Error while trying to add message to batch", err) } - // now let's send the batch + // After you add all the messages to the batch you send it using + // Sender.SendMessageBatch() err = sender.SendMessageBatch(context.TODO(), batch) exitOnError("Failed to send message batch", err) } @@ -65,8 +62,8 @@ func ExampleSender_ScheduleMessages() { // schedule the message to be delivered in an hour. sequenceNumbers, err := sender.ScheduleMessages(context.TODO(), - []azservicebus.SendableMessage{ - &azservicebus.Message{Body: []byte("hello world")}, + []*azservicebus.Message{ + {Body: []byte("hello world")}, }, time.Now().Add(time.Hour)) exitOnError("Failed to schedule messages", err) @@ -76,13 +73,11 @@ func ExampleSender_ScheduleMessages() { // or you can set the `ScheduledEnqueueTime` field on a message when you send it future := time.Now().Add(time.Hour) - err = sender.SendMessages(context.TODO(), - []*azservicebus.Message{ - { - Body: []byte("hello world"), - // schedule the message to be delivered in an hour. - ScheduledEnqueueTime: &future, - }, + err = sender.SendMessage(context.TODO(), + &azservicebus.Message{ + Body: []byte("hello world"), + // schedule the message to be delivered in an hour. + ScheduledEnqueueTime: &future, }) - exitOnError("Failed to schedule messages using SendMessages", err) + exitOnError("Failed to schedule messages using SendMessage", err) } diff --git a/sdk/messaging/azservicebus/example_shared_test.go b/sdk/messaging/azservicebus/example_shared_test.go index ded982a85986..b282d71b446e 100644 --- a/sdk/messaging/azservicebus/example_shared_test.go +++ b/sdk/messaging/azservicebus/example_shared_test.go @@ -26,7 +26,6 @@ func exitOnError(message string, err error) { var connectionString string var client *azservicebus.Client -var adminClient *azservicebus.AdminClient var sender *azservicebus.Sender var receiver *azservicebus.Receiver diff --git a/sdk/messaging/azservicebus/go.mod b/sdk/messaging/azservicebus/go.mod index c1057b715bcb..b5158c51f8f6 100644 --- a/sdk/messaging/azservicebus/go.mod +++ b/sdk/messaging/azservicebus/go.mod @@ -8,7 +8,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v0.20.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.12.0 github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1 - github.com/Azure/go-amqp v0.16.2 + github.com/Azure/go-amqp v0.16.3 github.com/Azure/go-autorest/autorest v0.11.18 github.com/Azure/go-autorest/autorest/adal v0.9.13 github.com/Azure/go-autorest/autorest/date v0.3.0 diff --git a/sdk/messaging/azservicebus/go.sum b/sdk/messaging/azservicebus/go.sum index 59b124b74f2d..faf99fd56aac 100644 --- a/sdk/messaging/azservicebus/go.sum +++ b/sdk/messaging/azservicebus/go.sum @@ -11,8 +11,8 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.12.0/go.mod h1:GJzjM4SR9T0Ky github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1 h1:BUYIbDf/mMZ8945v3QkG3OuqGVyS4Iek0AOLwdRAYoc= github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.1/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/go-amqp v0.16.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= -github.com/Azure/go-amqp v0.16.2 h1:3w03tFlEZ9sYTS1jgapvvJaxD2/6dXr52W2ElRaIriE= -github.com/Azure/go-amqp v0.16.2/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= +github.com/Azure/go-amqp v0.16.3 h1:zH2aVLvIn6tqRnYyviP/eqiZkbfPYBnvRE7fSz10SLY= +github.com/Azure/go-amqp v0.16.3/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.18 h1:90Y4srNYrwOtAgVo3ndrQkTYn6kf1Eg/AjTFJ8Is2aM= diff --git a/sdk/messaging/azservicebus/internal/amqpInterfaces.go b/sdk/messaging/azservicebus/internal/amqpInterfaces.go index 5f07aa7d8ebe..574d68b21823 100644 --- a/sdk/messaging/azservicebus/internal/amqpInterfaces.go +++ b/sdk/messaging/azservicebus/internal/amqpInterfaces.go @@ -16,6 +16,7 @@ type AMQPReceiver interface { IssueCredit(credit uint32) error DrainCredit(ctx context.Context) error Receive(ctx context.Context) (*amqp.Message, error) + Prefetched(ctx context.Context) (*amqp.Message, error) // settlement functions AcceptMessage(ctx context.Context, msg *amqp.Message) error diff --git a/sdk/messaging/azservicebus/internal/atom/entity_manager.go b/sdk/messaging/azservicebus/internal/atom/entity_manager.go index b049df60964f..5623f976f049 100644 --- a/sdk/messaging/azservicebus/internal/atom/entity_manager.go +++ b/sdk/messaging/azservicebus/internal/atom/entity_manager.go @@ -31,8 +31,15 @@ const ( ) type ( - // EntityManager provides CRUD functionality for Service Bus entities (Queues, Topics, Subscriptions...) - EntityManager struct { + EntityManager interface { + Get(ctx context.Context, entityPath string, respObj interface{}, mw ...MiddlewareFunc) (*http.Response, error) + Put(ctx context.Context, entityPath string, body interface{}, respObj interface{}, mw ...MiddlewareFunc) (*http.Response, error) + Delete(ctx context.Context, entityPath string, mw ...MiddlewareFunc) (*http.Response, error) + TokenProvider() auth.TokenProvider + } + + // entityManager provides CRUD functionality for Service Bus entities (Queues, Topics, Subscriptions...) + entityManager struct { tokenProvider auth.TokenProvider Host string mwStack []MiddlewareFunc @@ -145,7 +152,7 @@ func (m *managementError) String() string { // NewEntityManagerWithConnectionString creates an entity manager (a lower level HTTP client // for the ATOM endpoint). This is typically wrapped by an entity specific client (like // TopicManager, QueueManager or , SubscriptionManager). -func NewEntityManagerWithConnectionString(connectionString string, version string) (*EntityManager, error) { +func NewEntityManagerWithConnectionString(connectionString string, version string) (EntityManager, error) { parsed, err := conn.ParsedConnectionFromStr(connectionString) if err != nil { @@ -158,7 +165,7 @@ func NewEntityManagerWithConnectionString(connectionString string, version strin return nil, err } - return &EntityManager{ + return &entityManager{ Host: fmt.Sprintf("https://%s.%s/", parsed.Namespace, parsed.Suffix), version: version, tokenProvider: provider, @@ -172,8 +179,8 @@ func NewEntityManagerWithConnectionString(connectionString string, version strin } // NewEntityManager creates an entity manager using a TokenCredential. -func NewEntityManager(ns string, tokenCredential azcore.TokenCredential, version string) (*EntityManager, error) { - return &EntityManager{ +func NewEntityManager(ns string, tokenCredential azcore.TokenCredential, version string) (EntityManager, error) { + return &entityManager{ Host: fmt.Sprintf("https://%s/", ns), version: version, tokenProvider: sbauth.NewTokenProvider(tokenCredential), @@ -187,7 +194,7 @@ func NewEntityManager(ns string, tokenCredential azcore.TokenCredential, version } // Get performs an HTTP Get for a given entity path, deserializing the returned XML into `respObj` -func (em *EntityManager) Get(ctx context.Context, entityPath string, respObj interface{}, mw ...MiddlewareFunc) (*http.Response, error) { +func (em *entityManager) Get(ctx context.Context, entityPath string, respObj interface{}, mw ...MiddlewareFunc) (*http.Response, error) { ctx, span := em.startSpanFromContext(ctx, "sb.ATOM.Get") defer span.End() @@ -202,7 +209,7 @@ func (em *EntityManager) Get(ctx context.Context, entityPath string, respObj int } // Put performs an HTTP PUT for a given entity path and body, deserializing the returned XML into `respObj` -func (em *EntityManager) Put(ctx context.Context, entityPath string, body interface{}, respObj interface{}, mw ...MiddlewareFunc) (*http.Response, error) { +func (em *entityManager) Put(ctx context.Context, entityPath string, body interface{}, respObj interface{}, mw ...MiddlewareFunc) (*http.Response, error) { ctx, span := em.startSpanFromContext(ctx, "sb.ATOM.Put") defer span.End() @@ -223,14 +230,14 @@ func (em *EntityManager) Put(ctx context.Context, entityPath string, body interf } // Delete performs an HTTP DELETE for a given entity path -func (em *EntityManager) Delete(ctx context.Context, entityPath string, mw ...MiddlewareFunc) (*http.Response, error) { +func (em *entityManager) Delete(ctx context.Context, entityPath string, mw ...MiddlewareFunc) (*http.Response, error) { ctx, span := em.startSpanFromContext(ctx, "sb.ATOM.Delete") defer span.End() return em.execute(ctx, http.MethodDelete, entityPath, http.NoBody, mw...) } -func (em *EntityManager) execute(ctx context.Context, method string, entityPath string, body io.Reader, mw ...MiddlewareFunc) (*http.Response, error) { +func (em *entityManager) execute(ctx context.Context, method string, entityPath string, body io.Reader, mw ...MiddlewareFunc) (*http.Response, error) { ctx, span := em.startSpanFromContext(ctx, "sb.ATOM.Execute") defer span.End() @@ -295,12 +302,12 @@ func (em *EntityManager) execute(ctx context.Context, method string, entityPath } // Use adds middleware to the middleware mwStack -func (em *EntityManager) Use(mw ...MiddlewareFunc) { +func (em *entityManager) Use(mw ...MiddlewareFunc) { em.mwStack = append(em.mwStack, mw...) } // TokenProvider generates authorization tokens for communicating with the Service Bus management API -func (em *EntityManager) TokenProvider() auth.TokenProvider { +func (em *entityManager) TokenProvider() auth.TokenProvider { return em.tokenProvider } @@ -314,7 +321,7 @@ func FormatManagementError(body []byte, origErr error) error { return fmt.Errorf("error code: %d, Details: %s", mgmtError.Code, mgmtError.Detail) } -func (em *EntityManager) startSpanFromContext(ctx context.Context, operationName string) (context.Context, tab.Spanner) { +func (em *entityManager) startSpanFromContext(ctx context.Context, operationName string) (context.Context, tab.Spanner) { ctx, span := tab.StartSpan(ctx, operationName) tracing.ApplyComponentInfo(span, em.version) span.AddAttributes(tab.StringAttribute("span.kind", "client")) @@ -384,12 +391,42 @@ func TraceReqAndResponseMiddleware() MiddlewareFunc { } } -var errEntityDoesNotExist = errors.New("entity does not exist") +type feedEmptyError struct { + azcore.HTTPResponse + response *http.Response +} + +func (e feedEmptyError) RawResponse() *http.Response { + return e.response +} +func (e feedEmptyError) Error() string { + return "entity does not exist" +} + +func NotFound(err error) (bool, *http.Response) { + var feedEmptyError feedEmptyError + + if errors.As(err, &feedEmptyError) { + return true, feedEmptyError.RawResponse() + } -func NotFound(err error) bool { var httpResponse azcore.HTTPResponse - return errors.Is(err, errEntityDoesNotExist) || - (errors.As(err, &httpResponse) && httpResponse.RawResponse().StatusCode == 404) + + if errors.As(err, &httpResponse) { + return httpResponse.RawResponse().StatusCode == 404, httpResponse.RawResponse() + } + + return false, nil +} + +func AsHTTPResponse(err error) *http.Response { + var httpResponse azcore.HTTPResponse + + if errors.As(err, &httpResponse) { + return httpResponse.RawResponse() + } + + return nil } func isEmptyFeed(b []byte) bool { @@ -412,11 +449,10 @@ func deserializeBody(resp *http.Response, respObj interface{}) (*http.Response, } if err := xml.Unmarshal(bytes, respObj); err != nil { - // ATOM does this interesting thing where, when something doesn't exist, it gives you back an empty feed // check: if isEmptyFeed(bytes) { - return nil, errEntityDoesNotExist + return nil, feedEmptyError{response: resp} } return resp, err diff --git a/sdk/messaging/azservicebus/internal/atom/manager_common.go b/sdk/messaging/azservicebus/internal/atom/manager_common.go index e2627863e2aa..4686bc01b615 100644 --- a/sdk/messaging/azservicebus/internal/atom/manager_common.go +++ b/sdk/messaging/azservicebus/internal/atom/manager_common.go @@ -5,35 +5,13 @@ package atom import ( "context" - "fmt" "io" "io/ioutil" "net/http" - "net/url" "github.com/devigned/tab" ) -// constructAtomPath adds the proper parameters for skip and top -// This is common for the list operations for queues, topics and subscriptions. -func constructAtomPath(basePath string, skip int, top int) string { - values := url.Values{} - - if skip > 0 { - values.Add("$skip", fmt.Sprintf("%d", skip)) - } - - if top > 0 { - values.Add("$top", fmt.Sprintf("%d", top)) - } - - if len(values) == 0 { - return basePath - } - - return fmt.Sprintf("%s?%s", basePath, values.Encode()) -} - // CloseRes closes the response (or if it's nil just no-ops) func CloseRes(ctx context.Context, res *http.Response) { if res == nil { diff --git a/sdk/messaging/azservicebus/internal/atom/manager_common_test.go b/sdk/messaging/azservicebus/internal/atom/manager_common_test.go index 006a2ada2d4d..a23a2c7cacbd 100644 --- a/sdk/messaging/azservicebus/internal/atom/manager_common_test.go +++ b/sdk/messaging/azservicebus/internal/atom/manager_common_test.go @@ -11,23 +11,9 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestConstructAtomPath(t *testing.T) { - basePath := constructAtomPath("/something", 1, 2) - - // I'm assuming the ordering is non-deterministic since the underlying values are just a map - assert.Truef(t, basePath == "/something?%24skip=1&%24top=2" || basePath == "/something?%24top=2&%24skip=1", "%s wasn't one of our two variations", basePath) - - basePath = constructAtomPath("/something", 0, -1) - assert.EqualValues(t, "/something", basePath, "Values <= 0 are ignored") - - basePath = constructAtomPath("/something", -1, 0) - assert.EqualValues(t, "/something", basePath, "Values <= 0 are ignored") -} - // sanity check to make sure my error conforms to azcore's interface func TestResponseError(t *testing.T) { var err azcore.HTTPResponse = ResponseError{} diff --git a/sdk/messaging/azservicebus/internal/atom/models.go b/sdk/messaging/azservicebus/internal/atom/models.go index a4417fd9229e..b12b4c23a0fe 100644 --- a/sdk/messaging/azservicebus/internal/atom/models.go +++ b/sdk/messaging/azservicebus/internal/atom/models.go @@ -5,6 +5,7 @@ package atom import ( "encoding/xml" + "time" "github.com/Azure/go-autorest/autorest/date" ) @@ -63,6 +64,7 @@ type ( CountDetails *CountDetails `xml:"CountDetails,omitempty"` ForwardTo *string `xml:"ForwardTo,omitempty"` ForwardDeadLetteredMessagesTo *string `xml:"ForwardDeadLetteredMessagesTo,omitempty"` // ForwardDeadLetteredMessagesTo - absolute URI of the entity to forward dead letter messages + UserMetadata *string `xml:"UserMetadata,omitempty"` } ) @@ -226,3 +228,38 @@ type ( ID string } ) + +type ( + /* + + https://.servicebus.windows.net/$namespaceinfo?api-version=2017-04 + <my servicebus name> + 2021-11-07T23:41:24Z + + + + + + + 2019-12-03T22:18:04.09Z + Standard + 2021-08-19T23:37:00.75Z + + Messaging + + + + */ + + NamespaceEntry struct { + NamespaceInfo *NamespaceInfo `xml:"content>NamespaceInfo"` + } + + NamespaceInfo struct { + CreatedTime time.Time `xml:"CreatedTime"` + MessagingSKU string `xml:"MessagingSKU"` + MessagingUnits *int64 `xml:"MessagingUnits"` + ModifiedTime time.Time `xml:"ModifiedTime"` + Name string `xml:"Name"` + } +) diff --git a/sdk/messaging/azservicebus/internal/atom/topic.go b/sdk/messaging/azservicebus/internal/atom/topic.go index 7cb9323c24f9..47714693c056 100644 --- a/sdk/messaging/azservicebus/internal/atom/topic.go +++ b/sdk/messaging/azservicebus/internal/atom/topic.go @@ -24,6 +24,7 @@ type ( FilteringMessagesBeforePublishing *bool `xml:"FilteringMessagesBeforePublishing,omitempty"` IsAnonymousAccessible *bool `xml:"IsAnonymousAccessible,omitempty"` Status *EntityStatus `xml:"Status,omitempty"` + UserMetadata *string `xml:"UserMetadata,omitempty"` AccessedAt *date.Time `xml:"AccessedAt,omitempty"` CreatedAt *date.Time `xml:"CreatedAt,omitempty"` UpdatedAt *date.Time `xml:"UpdatedAt,omitempty"` diff --git a/sdk/messaging/azservicebus/internal/constants.go b/sdk/messaging/azservicebus/internal/constants.go index 02dbb1166fd0..ab8eee09ba8b 100644 --- a/sdk/messaging/azservicebus/internal/constants.go +++ b/sdk/messaging/azservicebus/internal/constants.go @@ -19,4 +19,4 @@ const ( // TODO: this should move into a proper file. Need to resolve some interdependency // issues between the public and internal packages first. // Version is the semantic version number -const Version = "v0.2.1" +const Version = "v0.3.0" diff --git a/sdk/messaging/azservicebus/internal/mgmt.go b/sdk/messaging/azservicebus/internal/mgmt.go index d513428962c5..a96200070a1e 100644 --- a/sdk/messaging/azservicebus/internal/mgmt.go +++ b/sdk/messaging/azservicebus/internal/mgmt.go @@ -49,7 +49,7 @@ type ( type MgmtClient interface { Close(ctx context.Context) error - SendDisposition(ctx context.Context, lockToken *amqp.UUID, state Disposition) error + SendDisposition(ctx context.Context, lockToken *amqp.UUID, state Disposition, propertiesToModify map[string]interface{}) error ReceiveDeferred(ctx context.Context, mode ReceiveMode, sequenceNumbers []int64) ([]*amqp.Message, error) PeekMessages(ctx context.Context, fromSequenceNumber int64, messageCount int32) ([]*amqp.Message, error) @@ -562,7 +562,7 @@ func (mc *mgmtClient) SetSessionState(ctx context.Context, sessionID string, sta // SendDisposition allows you settle a message using the management link, rather than via your // *amqp.Receiver. Use this if the receiver has been closed/lost or if the message isn't associated // with a link (ex: deferred messages). -func (mc *mgmtClient) SendDisposition(ctx context.Context, lockToken *amqp.UUID, state Disposition) error { +func (mc *mgmtClient) SendDisposition(ctx context.Context, lockToken *amqp.UUID, state Disposition, propertiesToModify map[string]interface{}) error { ctx, span := tracing.StartConsumerSpanFromContext(ctx, tracing.SpanSendDisposition, Version) defer span.End() @@ -586,6 +586,10 @@ func (mc *mgmtClient) SendDisposition(ctx context.Context, lockToken *amqp.UUID, value["deadletter-description"] = state.DeadLetterDescription } + if propertiesToModify != nil { + value["properties-to-modify"] = propertiesToModify + } + msg := &amqp.Message{ ApplicationProperties: map[string]interface{}{ "operation": "com.microsoft:update-disposition", diff --git a/sdk/messaging/azservicebus/stress/Dockerfile b/sdk/messaging/azservicebus/internal/stress/Dockerfile similarity index 100% rename from sdk/messaging/azservicebus/stress/Dockerfile rename to sdk/messaging/azservicebus/internal/stress/Dockerfile diff --git a/sdk/messaging/azservicebus/stress/buildstress.bat b/sdk/messaging/azservicebus/internal/stress/buildstress.bat similarity index 100% rename from sdk/messaging/azservicebus/stress/buildstress.bat rename to sdk/messaging/azservicebus/internal/stress/buildstress.bat diff --git a/sdk/messaging/azservicebus/internal/stress/finiteSendAndReceive.go b/sdk/messaging/azservicebus/internal/stress/finiteSendAndReceive.go new file mode 100644 index 000000000000..5d8781910aac --- /dev/null +++ b/sdk/messaging/azservicebus/internal/stress/finiteSendAndReceive.go @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package main + +import ( + "context" + "fmt" + "log" + "strings" + "sync/atomic" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" + "github.com/microsoft/ApplicationInsights-Go/appinsights" +) + +func finiteSendAndReceiveTest(cs string, telemetryClient appinsights.TelemetryClient) { + telemetryClient.TrackEvent("Start") + defer telemetryClient.TrackEvent("End") + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + stats := &stats{} + + // create a new queue + adminClient, err := admin.NewClientFromConnectionString(cs, nil) + + if err != nil { + trackError(stats, telemetryClient, "failed to create adminclient", err) + panic(err) + } + + queueName := strings.ToLower(fmt.Sprintf("queue-%X", time.Now().UnixNano())) + + log.Printf("Creating queue") + + _, err = adminClient.CreateQueue(context.Background(), queueName, nil, nil) + + if err != nil { + trackError(stats, telemetryClient, fmt.Sprintf("failed to create queue %s", queueName), err) + panic(err) + } + + defer func() { + _, err = adminClient.DeleteQueue(context.Background(), queueName, nil) + + if err != nil { + trackError(stats, telemetryClient, fmt.Sprintf("failed to create queue %s", queueName), err) + } + }() + + client, err := azservicebus.NewClientFromConnectionString(cs, nil) + + if err != nil { + trackError(stats, telemetryClient, "failed to create client", err) + panic(err) + } + + sender, err := client.NewSender(queueName, nil) + + if err != nil { + trackError(stats, telemetryClient, "failed to create client", err) + panic(err) + } + + startStatsTicker(ctx, "finitesr", stats, 5*time.Second) + + const messageLimit = 10000 + + log.Printf("Sending %d messages", messageLimit) + + for i := 0; i < messageLimit; i++ { + err := sender.SendMessage(context.Background(), &azservicebus.Message{ + Body: []byte(fmt.Sprintf("Message %d", i)), + }) + + if err != nil { + trackError(stats, telemetryClient, "failed to create client", err) + panic(err) + } + + atomic.AddInt32(&stats.Sent, 1) + } + + log.Printf("Starting receiving...") + + receiver, err := client.NewReceiverForQueue(queueName, nil) + + var all []*azservicebus.ReceivedMessage + + for { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + messages, err := receiver.ReceiveMessages(context.Background(), 100, nil) + + if err != nil { + trackError(stats, telemetryClient, "failed to create client", err) + panic(err) + } + + for _, msg := range messages { + if err := receiver.CompleteMessage(ctx, msg); err != nil { + trackError(stats, telemetryClient, "failed to create client", err) + panic(err) + } + } + + atomic.AddInt32(&stats.Received, int32(len(messages))) + all = append(all, messages...) + + if len(all) == messageLimit { + log.Printf("All messages received!") + break + } + } +} diff --git a/sdk/messaging/azservicebus/stress/go-secret.yaml b/sdk/messaging/azservicebus/internal/stress/go-secret.yaml similarity index 100% rename from sdk/messaging/azservicebus/stress/go-secret.yaml rename to sdk/messaging/azservicebus/internal/stress/go-secret.yaml diff --git a/sdk/messaging/azservicebus/stress/job.yaml b/sdk/messaging/azservicebus/internal/stress/job.yaml similarity index 100% rename from sdk/messaging/azservicebus/stress/job.yaml rename to sdk/messaging/azservicebus/internal/stress/job.yaml diff --git a/sdk/messaging/azservicebus/stress/stress.go b/sdk/messaging/azservicebus/internal/stress/stress.go similarity index 72% rename from sdk/messaging/azservicebus/stress/stress.go rename to sdk/messaging/azservicebus/internal/stress/stress.go index 6fc161ccc02e..c761e7b509d5 100644 --- a/sdk/messaging/azservicebus/stress/stress.go +++ b/sdk/messaging/azservicebus/internal/stress/stress.go @@ -12,6 +12,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/tracing" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/utils" "github.com/devigned/tab" @@ -36,10 +37,6 @@ var receiverStats stats var senderStats stats func main() { - runBasicSendAndReceiveTest() -} - -func runBasicSendAndReceiveTest() { err := godotenv.Load() if err != nil { @@ -54,10 +51,45 @@ func runBasicSendAndReceiveTest() { } config := appinsights.NewTelemetryConfiguration(aiKey) - config.MaxBatchInterval = 5 * time.Second telemetryClient := appinsights.NewTelemetryClientFromConfig(config) + if len(os.Args) == 1 { + runBasicSendAndReceiveTest(cs, telemetryClient) + } + + switch os.Args[1] { + case "finiteSendAndReceive": + finiteSendAndReceiveTest(cs, telemetryClient) + default: + log.Printf("Unknown test: %s", os.Args[0]) + } +} + +func startStatsTicker(ctx context.Context, prefix string, stats *stats, interval time.Duration) { + go func(ctx context.Context) { + ticker := time.NewTicker(interval) + + TickerLoop: + for range ticker.C { + select { + case <-ctx.Done(): + ticker.Stop() + break TickerLoop + default: + } + + log.Printf("[%s] Received: (r:%d), Sent: %d, Errors: %d", + prefix, + atomic.LoadInt32(&stats.Received), + atomic.LoadInt32(&stats.Sent), + atomic.LoadInt32(&stats.Errors), + ) + } + }(ctx) +} + +func runBasicSendAndReceiveTest(cs string, telemetryClient appinsights.TelemetryClient) { go func() { ticker := time.NewTicker(5 * time.Second) @@ -106,13 +138,13 @@ func runBasicSendAndReceiveTest() { serviceBusClient, err := azservicebus.NewClientFromConnectionString(cs, nil) if err != nil { - trackException(nil, telemetryClient, "Failed to create service bus client", err) + trackError(nil, telemetryClient, "Failed to create service bus client", err) return } defer func() { if err := serviceBusClient.Close(ctx); err != nil { - trackException(nil, telemetryClient, "Error when closing client", err) + trackError(nil, telemetryClient, "Error when closing client", err) } }() @@ -156,7 +188,7 @@ func runBatchReceiver(ctx context.Context, serviceBusClient *azservicebus.Client messages, err := receiver.ReceiveMessages(ctx, 20, nil) if err != nil { - trackException(&receiverStats, telemetryClient, "receive batch failure", err) + trackError(&receiverStats, telemetryClient, "receive batch failure", err) } atomic.AddInt32(&receiverStats.Received, int32(len(messages))) @@ -164,7 +196,7 @@ func runBatchReceiver(ctx context.Context, serviceBusClient *azservicebus.Client for _, msg := range messages { go func(msg *azservicebus.ReceivedMessage) { if err := receiver.CompleteMessage(ctx, msg); err != nil { - trackException(&receiverStats, telemetryClient, "complete failed", err) + trackError(&receiverStats, telemetryClient, "complete failed", err) } }(msg) } @@ -172,10 +204,10 @@ func runBatchReceiver(ctx context.Context, serviceBusClient *azservicebus.Client } func continuallySend(ctx context.Context, client *azservicebus.Client, queueName string, telemetryClient appinsights.TelemetryClient) { - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) if err != nil { - trackException(&senderStats, telemetryClient, "SenderCreate", err) + trackError(&senderStats, telemetryClient, "SenderCreate", err) return } @@ -198,7 +230,7 @@ func continuallySend(ctx context.Context, client *azservicebus.Client, queueName break } - trackException(&senderStats, telemetryClient, "SendMessage", err) + trackError(&senderStats, telemetryClient, "SendMessage", err) break } } @@ -208,32 +240,32 @@ func createSubscriptions(telemetryClient appinsights.TelemetryClient, connection log.Printf("[BEGIN] Creating topic %s", topicName) defer log.Printf("[END] Creating topic %s", topicName) - ac, err := azservicebus.NewAdminClientWithConnectionString(connectionString, nil) + ac, err := admin.NewClientFromConnectionString(connectionString, nil) if err != nil { - trackException(nil, telemetryClient, "Failed to create a topic manager", err) + trackError(nil, telemetryClient, "Failed to create a topic manager", err) return nil, err } - if _, err := ac.AddTopic(context.Background(), topicName); err != nil { - trackException(nil, telemetryClient, "Failed to create topic", err) + if _, err := ac.CreateTopic(context.Background(), topicName, nil, nil); err != nil { + trackError(nil, telemetryClient, "Failed to create topic", err) return nil, err } for _, name := range subscriptionNames { - if _, err := ac.AddSubscription(context.Background(), topicName, name); err != nil { - trackException(nil, telemetryClient, "Failed to create subscription manager", err) + if _, err := ac.CreateSubscription(context.Background(), topicName, name, nil, nil); err != nil { + trackError(nil, telemetryClient, "Failed to create subscription manager", err) } } return func() { - if _, err := ac.DeleteTopic(context.Background(), topicName); err != nil { - trackException(nil, telemetryClient, fmt.Sprintf("Failed to delete topic %s", topicName), err) + if _, err := ac.DeleteTopic(context.Background(), topicName, nil); err != nil { + trackError(nil, telemetryClient, fmt.Sprintf("Failed to delete topic %s", topicName), err) } }, nil } -func trackException(stats *stats, telemetryClient appinsights.TelemetryClient, message string, err error) { +func trackError(stats *stats, telemetryClient appinsights.TelemetryClient, message string, err error) { log.Printf("Exception: %s: %s", message, err.Error()) if stats != nil { diff --git a/sdk/messaging/azservicebus/internal/test/test.go b/sdk/messaging/azservicebus/internal/test/test_helpers.go similarity index 94% rename from sdk/messaging/azservicebus/internal/test/test.go rename to sdk/messaging/azservicebus/internal/test/test_helpers.go index 685b3ac428ff..f6b3634d3609 100644 --- a/sdk/messaging/azservicebus/internal/test/test.go +++ b/sdk/messaging/azservicebus/internal/test/test_helpers.go @@ -8,6 +8,7 @@ import ( "io" "math/rand" "os" + "testing" "time" "github.com/Azure/azure-amqp-common-go/v3/conn" @@ -196,3 +197,23 @@ func RandomString(prefix string, length int) string { } return prefix + string(b) } + +func GetConnectionString(t *testing.T) string { + cs := os.Getenv("SERVICEBUS_CONNECTION_STRING") + + if cs == "" { + t.Skip() + } + + return cs +} + +func GetConnectionStringWithoutManagePerms(t *testing.T) string { + cs := os.Getenv("SERVICEBUS_CONNECTION_STRING_NO_MANAGE") + + if cs == "" { + t.Skip() + } + + return cs +} diff --git a/sdk/messaging/azservicebus/liveTestHelpers_test.go b/sdk/messaging/azservicebus/liveTestHelpers_test.go index c754a8b34dc6..850fb5afb504 100644 --- a/sdk/messaging/azservicebus/liveTestHelpers_test.go +++ b/sdk/messaging/azservicebus/liveTestHelpers_test.go @@ -6,15 +6,16 @@ package azservicebus import ( "context" "fmt" - "os" "testing" "time" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal/test" "github.com/stretchr/testify/require" ) -func setupLiveTest(t *testing.T, props *QueueProperties) (*Client, func(), string) { - cs := getConnectionString(t) +func setupLiveTest(t *testing.T, props *admin.QueueProperties) (*Client, func(), string) { + cs := test.GetConnectionString(t) serviceBusClient, err := NewClientFromConnectionString(cs, nil) require.NoError(t, err) @@ -30,47 +31,34 @@ func setupLiveTest(t *testing.T, props *QueueProperties) (*Client, func(), strin return serviceBusClient, testCleanup, queueName } -func getConnectionString(t *testing.T) string { - cs := os.Getenv("SERVICEBUS_CONNECTION_STRING") - - if cs == "" { - t.Skip() - } - - return cs -} - -func getConnectionStringWithoutManagePerms(t *testing.T) string { - cs := os.Getenv("SERVICEBUS_CONNECTION_STRING_NO_MANAGE") - - if cs == "" { - t.Skip() - } - - return cs -} - // createQueue creates a queue using a subset of entries in 'queueDescription': // - EnablePartitioning // - RequiresSession -func createQueue(t *testing.T, connectionString string, queueProperties *QueueProperties) (string, func()) { +func createQueue(t *testing.T, connectionString string, queueProperties *admin.QueueProperties) (string, func()) { nanoSeconds := time.Now().UnixNano() queueName := fmt.Sprintf("queue-%X", nanoSeconds) - adminClient, err := NewAdminClientWithConnectionString(connectionString, nil) + adminClient, err := admin.NewClientFromConnectionString(connectionString, nil) require.NoError(t, err) if queueProperties == nil { - queueProperties = &QueueProperties{} + queueProperties = &admin.QueueProperties{} } - queueProperties.Name = queueName - _, err = adminClient.AddQueueWithProperties(context.Background(), queueProperties) + _, err = adminClient.CreateQueue(context.Background(), queueName, queueProperties, nil) require.NoError(t, err) return queueName, func() { - if _, err := adminClient.DeleteQueue(context.TODO(), queueProperties.Name); err != nil { - require.NoError(t, err) - } + deleteQueue(t, adminClient, queueName) } } + +func deleteQueue(t *testing.T, ac *admin.Client, queueName string) { + _, err := ac.DeleteQueue(context.Background(), queueName, nil) + require.NoError(t, err) +} + +func deleteSubscription(t *testing.T, ac *admin.Client, topicName string, subscriptionName string) { + _, err := ac.DeleteSubscription(context.Background(), topicName, subscriptionName, nil) + require.NoError(t, err) +} diff --git a/sdk/messaging/azservicebus/message.go b/sdk/messaging/azservicebus/message.go index 7365c5e0ed30..f43175c69674 100644 --- a/sdk/messaging/azservicebus/message.go +++ b/sdk/messaging/azservicebus/message.go @@ -5,6 +5,7 @@ package azservicebus import ( "context" + "errors" "fmt" "time" @@ -14,53 +15,81 @@ import ( "github.com/devigned/tab" ) -type ( - // ReceivedMessage is a received message from a Client.NewReceiver(). - ReceivedMessage struct { - Message +// ReceivedMessage is a received message from a Client.NewReceiver(). +type ReceivedMessage struct { + MessageID string - LockToken [16]byte - DeliveryCount uint32 - LockedUntil *time.Time // `mapstructure:"x-opt-locked-until"` - SequenceNumber *int64 // :"x-opt-sequence-number"` - EnqueuedSequenceNumber *int64 // :"x-opt-enqueue-sequence-number"` - EnqueuedTime *time.Time // :"x-opt-enqueued-time"` - DeadLetterSource *string // :"x-opt-deadletter-source"` + ContentType string + CorrelationID string + SessionID *string + Subject string + ReplyTo string + ReplyToSessionID string + To string - // available in the raw AMQP message, but not exported by default - // GroupSequence *uint32 + TimeToLive *time.Duration - rawAMQPMessage *amqp.Message + PartitionKey *string + TransactionPartitionKey *string + ScheduledEnqueueTime *time.Time - // deferred indicates we received it using ReceiveDeferredMessages. These messages - // will still go through the normal Receiver.Settle functions but internally will - // always be settled with the management link. - deferred bool - } + ApplicationProperties map[string]interface{} + + LockToken [16]byte + DeliveryCount uint32 + LockedUntil *time.Time + SequenceNumber *int64 + EnqueuedSequenceNumber *int64 + EnqueuedTime *time.Time + ExpiresAt *time.Time + + DeadLetterErrorDescription *string + DeadLetterReason *string + DeadLetterSource *string + + // available in the raw AMQP message, but not exported by default + // GroupSequence *uint32 + + rawAMQPMessage *amqp.Message - // Message is a SendableMessage which can be sent using a Client.NewSender(). - Message struct { - ID string - - ContentType string - CorrelationID string - // Body corresponds to the first []byte array in the Data section of an AMQP message. - Body []byte - SessionID *string - Subject string - ReplyTo string - ReplyToSessionID string - To string - TimeToLive *time.Duration - - PartitionKey *string - TransactionPartitionKey *string - ScheduledEnqueueTime *time.Time - - ApplicationProperties map[string]interface{} - Format uint32 + // deferred indicates we received it using ReceiveDeferredMessages. These messages + // will still go through the normal Receiver.Settle functions but internally will + // always be settled with the management link. + deferred bool +} + +// Body returns the body for this received message. +// If the body not compatible with ReceivedMessage this function will return an error. +func (rm *ReceivedMessage) Body() ([]byte, error) { + // TODO: does this come back as a zero length array if the body is empty (which is allowed) + if rm.rawAMQPMessage.Data == nil || len(rm.rawAMQPMessage.Data) != 1 { + return nil, errors.New("AMQP message Data section is improperly encoded for ReceivedMessage") } -) + + return rm.rawAMQPMessage.Data[0], nil +} + +// Message is a SendableMessage which can be sent using a Client.NewSender(). +type Message struct { + MessageID string + + ContentType string + CorrelationID string + // Body corresponds to the first []byte array in the Data section of an AMQP message. + Body []byte + SessionID *string + Subject string + ReplyTo string + ReplyToSessionID string + To string + TimeToLive *time.Duration + + PartitionKey *string + TransactionPartitionKey *string + ScheduledEnqueueTime *time.Time + + ApplicationProperties map[string]interface{} +} // Service Bus custom properties const ( @@ -78,10 +107,6 @@ const ( enqueuedSequenceNumberAnnotation = "x-opt-enqueue-sequence-number" ) -func (m *Message) messageType() string { - return "Message" -} - func (m *Message) toAMQPMessage() *amqp.Message { amqpMsg := amqp.NewMessage(m.Body) @@ -94,7 +119,7 @@ func (m *Message) toAMQPMessage() *amqp.Message { // TODO: I don't think this should be strictly required. Need to // look into why it won't send properly without one. - var messageID = m.ID + var messageID = m.MessageID if messageID == "" { uuid, err := uuid.NewV4() @@ -174,15 +199,12 @@ func (m *Message) toAMQPMessage() *amqp.Message { // serialized byte array in the Data section of the messsage. func newReceivedMessage(ctxForLogging context.Context, amqpMsg *amqp.Message) *ReceivedMessage { msg := &ReceivedMessage{ - Message: Message{ - Body: amqpMsg.GetData(), - }, rawAMQPMessage: amqpMsg, } if amqpMsg.Properties != nil { if id, ok := amqpMsg.Properties.MessageID.(string); ok { - msg.ID = id + msg.MessageID = id } msg.SessionID = &amqpMsg.Properties.GroupID //msg.GroupSequence = &amqpMsg.Properties.GroupSequence @@ -206,6 +228,14 @@ func newReceivedMessage(ctxForLogging context.Context, amqpMsg *amqp.Message) *R for key, value := range amqpMsg.ApplicationProperties { msg.ApplicationProperties[key] = value } + + if deadLetterErrorDescription, ok := amqpMsg.ApplicationProperties["DeadLetterErrorDescription"]; ok { + msg.DeadLetterErrorDescription = to.StringPtr(deadLetterErrorDescription.(string)) + } + + if deadLetterReason, ok := amqpMsg.ApplicationProperties["DeadLetterReason"]; ok { + msg.DeadLetterReason = to.StringPtr(deadLetterReason.(string)) + } } if amqpMsg.Annotations != nil { @@ -286,7 +316,11 @@ func newReceivedMessage(ctxForLogging context.Context, amqpMsg *amqp.Message) *R } } - msg.Format = amqpMsg.Format + if msg.EnqueuedTime != nil && msg.TimeToLive != nil { + expiresAt := msg.EnqueuedTime.Add(*msg.TimeToLive) + msg.ExpiresAt = &expiresAt + } + return msg } diff --git a/sdk/messaging/azservicebus/messageBatch.go b/sdk/messaging/azservicebus/messageBatch.go index 66718e5e0089..8d4b2c2ca76f 100644 --- a/sdk/messaging/azservicebus/messageBatch.go +++ b/sdk/messaging/azservicebus/messageBatch.go @@ -4,17 +4,22 @@ package azservicebus import ( + "errors" + "github.com/Azure/azure-amqp-common-go/v3/uuid" "github.com/Azure/go-amqp" ) +// ErrMessageTooLarge is returned when a message cannot fit into a batch when using MessageBatch.Add() +var ErrMessageTooLarge = errors.New("the message could not be added because it is too large for the batch") + type ( // MessageBatch represents a batch of messages to send to Service Bus in a single message MessageBatch struct { marshaledMessages [][]byte batchEnvelope *amqp.Message - maxBytes int - size int + maxBytes int32 + size int32 } ) @@ -22,11 +27,11 @@ const ( batchMessageFormat uint32 = 0x80013700 // TODO: should be calculated, not just a constant. - batchMessageWrapperSize = 100 + batchMessageWrapperSize = int32(100) ) // NewMessageBatch builds a new message batch with a default standard max message size -func newMessageBatch(maxBytes int) *MessageBatch { +func newMessageBatch(maxBytes int32) *MessageBatch { mb := &MessageBatch{ maxBytes: maxBytes, } @@ -35,17 +40,41 @@ func newMessageBatch(maxBytes int) *MessageBatch { } // Add adds a message to the batch if the message will not exceed the max size of the batch -// This function will return: -// (true, nil) if the message was added. -// (false, nil) if the message was too large to fit into the batch. -// (false, err) if an error occurs when adding the message. -func (mb *MessageBatch) Add(m SendableMessage) (bool, error) { - msg := m.toAMQPMessage() +// Returns: +// - ErrMessageTooLarge if the message cannot fit +// - a non-nil error for other failures +// - nil, otherwise +func (mb *MessageBatch) AddMessage(m *Message) error { + return mb.addAMQPMessage(m.toAMQPMessage()) +} + +// NumBytes is the number of bytes in the message batch +func (mb *MessageBatch) NumBytes() int32 { + // calculated data size + batch message wrapper + data wrapper portions of the message + return mb.size + batchMessageWrapperSize + (int32(len(mb.marshaledMessages)) * 5) +} + +// NumMessages returns the # of messages in the batch. +func (mb *MessageBatch) NumMessages() int32 { + return int32(len(mb.marshaledMessages)) +} + +// toAMQPMessage converts this batch into a sendable *amqp.Message +// NOTE: not idempotent! +func (mb *MessageBatch) toAMQPMessage() *amqp.Message { + mb.batchEnvelope.Data = make([][]byte, len(mb.marshaledMessages)) + mb.batchEnvelope.Format = batchMessageFormat + + copy(mb.batchEnvelope.Data, mb.marshaledMessages) + return mb.batchEnvelope +} + +func (mb *MessageBatch) addAMQPMessage(msg *amqp.Message) error { if msg.Properties.MessageID == nil || msg.Properties.MessageID == "" { uid, err := uuid.NewV4() if err != nil { - return false, err + return err } msg.Properties.MessageID = uid.String() } @@ -56,48 +85,33 @@ func (mb *MessageBatch) Add(m SendableMessage) (bool, error) { bin, err := msg.MarshalBinary() if err != nil { - return false, err + return err } - if mb.Size()+len(bin) > int(mb.maxBytes) { - return false, nil + if int(mb.NumBytes())+len(bin) > int(mb.maxBytes) { + return ErrMessageTooLarge } - mb.size += len(bin) + mb.size += int32(len(bin)) if len(mb.marshaledMessages) == 0 { // first message, store it since we need to copy attributes from it // when we send the overall batch message. - amqpMessage := m.toAMQPMessage() - - // we don't need to hold onto this since it'll get encoded - // into our marshaledMessages. We just want the metadata. - amqpMessage.Data = nil - mb.batchEnvelope = amqpMessage + mb.batchEnvelope = createBatchEnvelope(msg) } mb.marshaledMessages = append(mb.marshaledMessages, bin) - return true, nil -} - -// Size is the number of bytes in the message batch -func (mb *MessageBatch) Size() int { - // calculated data size + batch message wrapper + data wrapper portions of the message - return mb.size + batchMessageWrapperSize + (len(mb.marshaledMessages) * 5) -} - -// Len returns the # of messages in the batch. -func (mb *MessageBatch) Len() int { - return len(mb.marshaledMessages) + return nil } -// toAMQPMessage converts this batch into a sendable *amqp.Message -// NOTE: not idempotent! -func (mb *MessageBatch) toAMQPMessage() *amqp.Message { - mb.batchEnvelope.Data = make([][]byte, len(mb.marshaledMessages)) - mb.batchEnvelope.Format = batchMessageFormat +// createBatchEnvelope makes a copy of the properties of the message, minus any +// payload fields (like Data, Value or (eventually) Sequence). The data field will be +// filled in with all the messages when the batch is completed. +func createBatchEnvelope(am *amqp.Message) *amqp.Message { + newAMQPMessage := *am - copy(mb.batchEnvelope.Data, mb.marshaledMessages) + newAMQPMessage.Data = nil + newAMQPMessage.Value = nil - return mb.batchEnvelope + return &newAMQPMessage } diff --git a/sdk/messaging/azservicebus/messageBatch_test.go b/sdk/messaging/azservicebus/messageBatch_test.go index 038af038890d..0aa35f838245 100644 --- a/sdk/messaging/azservicebus/messageBatch_test.go +++ b/sdk/messaging/azservicebus/messageBatch_test.go @@ -13,24 +13,22 @@ func TestMessageBatchUnitTests(t *testing.T) { t.Run("addMessages", func(t *testing.T) { mb := newMessageBatch(1000) - added, err := mb.Add(&Message{ + err := mb.AddMessage(&Message{ Body: []byte("hello world"), }) require.NoError(t, err) - require.True(t, added) - require.EqualValues(t, 1, mb.Len()) - require.EqualValues(t, 195, mb.Size()) + require.EqualValues(t, 1, mb.NumMessages()) + require.EqualValues(t, 195, mb.NumBytes()) }) t.Run("addTooManyMessages", func(t *testing.T) { mb := newMessageBatch(1) - added, err := mb.Add(&Message{ + err := mb.AddMessage(&Message{ Body: []byte("hello world"), }) - require.False(t, added) - require.Nil(t, err) + require.EqualError(t, err, ErrMessageTooLarge.Error()) }) } diff --git a/sdk/messaging/azservicebus/messageSettler.go b/sdk/messaging/azservicebus/messageSettler.go index 088fe5132049..9059518d3910 100644 --- a/sdk/messaging/azservicebus/messageSettler.go +++ b/sdk/messaging/azservicebus/messageSettler.go @@ -15,8 +15,8 @@ var errReceiveAndDeleteReceiver = errors.New("messages that are received in rece type settler interface { CompleteMessage(ctx context.Context, message *ReceivedMessage) error - AbandonMessage(ctx context.Context, message *ReceivedMessage) error - DeferMessage(ctx context.Context, message *ReceivedMessage) error + AbandonMessage(ctx context.Context, message *ReceivedMessage, options *AbandonMessageOptions) error + DeferMessage(ctx context.Context, message *ReceivedMessage, options *DeferMessageOptions) error DeadLetterMessage(ctx context.Context, message *ReceivedMessage, options *DeadLetterOptions) error } @@ -76,41 +76,77 @@ func (s *messageSettler) settleWithRetries(ctx context.Context, message *Receive func (s *messageSettler) CompleteMessage(ctx context.Context, message *ReceivedMessage) error { return s.settleWithRetries(ctx, message, func(receiver internal.AMQPReceiver, mgmt internal.MgmtClient) error { if s.useManagementLink(message, receiver) { - return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), internal.Disposition{Status: internal.CompletedDisposition}) + return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), internal.Disposition{Status: internal.CompletedDisposition}, nil) } else { return receiver.AcceptMessage(ctx, message.rawAMQPMessage) } }) } +type AbandonMessageOptions struct { + // PropertiesToModify specifies properties to modify in the message when it is abandoned. + PropertiesToModify map[string]interface{} +} + // AbandonMessage will cause a message to be returned to the queue or subscription. // This will increment its delivery count, and potentially cause it to be dead lettered // depending on your queue or subscription's configuration. -func (s *messageSettler) AbandonMessage(ctx context.Context, message *ReceivedMessage) error { +func (s *messageSettler) AbandonMessage(ctx context.Context, message *ReceivedMessage, options *AbandonMessageOptions) error { return s.settleWithRetries(ctx, message, func(receiver internal.AMQPReceiver, mgmt internal.MgmtClient) error { if s.useManagementLink(message, receiver) { d := internal.Disposition{ Status: internal.AbandonedDisposition, } - return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), d) + + var propertiesToModify map[string]interface{} + + if options != nil && options.PropertiesToModify != nil { + propertiesToModify = options.PropertiesToModify + } + + return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), d, propertiesToModify) } - return receiver.ModifyMessage(ctx, message.rawAMQPMessage, false, false, nil) + var annotations amqp.Annotations + + if options != nil { + annotations = newAnnotations(options.PropertiesToModify) + } + + return receiver.ModifyMessage(ctx, message.rawAMQPMessage, false, false, annotations) }) } +type DeferMessageOptions struct { + // PropertiesToModify specifies properties to modify in the message when it is deferred + PropertiesToModify map[string]interface{} +} + // DeferMessage will cause a message to be deferred. Deferred messages // can be received using `Receiver.ReceiveDeferredMessages`. -func (s *messageSettler) DeferMessage(ctx context.Context, message *ReceivedMessage) error { +func (s *messageSettler) DeferMessage(ctx context.Context, message *ReceivedMessage, options *DeferMessageOptions) error { return s.settleWithRetries(ctx, message, func(receiver internal.AMQPReceiver, mgmt internal.MgmtClient) error { if s.useManagementLink(message, receiver) { d := internal.Disposition{ Status: internal.DeferredDisposition, } - return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), d) + + var propertiesToModify map[string]interface{} + + if options != nil && options.PropertiesToModify != nil { + propertiesToModify = options.PropertiesToModify + } + + return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), d, propertiesToModify) + } + + var annotations amqp.Annotations + + if options != nil { + annotations = newAnnotations(options.PropertiesToModify) } - return receiver.ModifyMessage(ctx, message.rawAMQPMessage, false, true, nil) + return receiver.ModifyMessage(ctx, message.rawAMQPMessage, false, true, annotations) }) } @@ -151,7 +187,14 @@ func (s *messageSettler) DeadLetterMessage(ctx context.Context, message *Receive DeadLetterDescription: &description, DeadLetterReason: &reason, } - return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), d) + + var propertiesToModify map[string]interface{} + + if options != nil && options.PropertiesToModify != nil { + propertiesToModify = options.PropertiesToModify + } + + return mgmt.SendDisposition(ctx, bytesToAMQPUUID(message.LockToken), d, propertiesToModify) } info := map[string]interface{}{ @@ -178,3 +221,17 @@ func bytesToAMQPUUID(bytes [16]byte) *amqp.UUID { uuid := amqp.UUID(bytes) return &uuid } + +func newAnnotations(propertiesToModify map[string]interface{}) amqp.Annotations { + var annotations amqp.Annotations + + for k, v := range propertiesToModify { + if annotations == nil { + annotations = amqp.Annotations{} + } + + annotations[k] = v + } + + return annotations +} diff --git a/sdk/messaging/azservicebus/messageSettler_test.go b/sdk/messaging/azservicebus/messageSettler_test.go index 96aba645c4cb..189a91c3e2ca 100644 --- a/sdk/messaging/azservicebus/messageSettler_test.go +++ b/sdk/messaging/azservicebus/messageSettler_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/stretchr/testify/require" ) @@ -28,21 +29,34 @@ func TestMessageSettlementUsingReceiver(t *testing.T) { require.EqualValues(t, 1, msg.DeliveryCount) // message from queue -> Abandon -> back to the queue - err = receiver.AbandonMessage(context.Background(), msg) + err = receiver.AbandonMessage(context.Background(), msg, &AbandonMessageOptions{ + PropertiesToModify: map[string]interface{}{ + "hello": "world", + }, + }) require.NoError(t, err) msg, err = receiver.receiveMessage(ctx, nil) require.NoError(t, err) require.EqualValues(t, 2, msg.DeliveryCount) + require.EqualValues(t, "world", msg.ApplicationProperties["hello"].(string)) // message from queue -> DeadLetter -> to the dead letter queue - err = receiver.DeadLetterMessage(ctx, msg, nil) + err = receiver.DeadLetterMessage(ctx, msg, &DeadLetterOptions{ + ErrorDescription: to.StringPtr("the error description"), + Reason: to.StringPtr("the error reason"), + }) require.NoError(t, err) msg, err = deadLetterReceiver.receiveMessage(ctx, nil) require.NoError(t, err) require.EqualValues(t, 2, msg.DeliveryCount) + require.EqualValues(t, "the error description", *msg.DeadLetterErrorDescription) + require.EqualValues(t, "the error reason", *msg.DeadLetterReason) + + require.EqualValues(t, *msg.ExpiresAt, msg.EnqueuedTime.Add(*msg.TimeToLive)) + // TODO: introducing deferred messages into the chain seems to have broken something. // // message from dead letter queue -> Defer -> to the dead letter queue's deferred messages // err = deadLetterReceiver.DeferMessage(ctx, msg) @@ -52,12 +66,17 @@ func TestMessageSettlementUsingReceiver(t *testing.T) { // require.NoError(t, err) // deferred message from dead letter queue -> Abandon -> dead letter queue - err = deadLetterReceiver.AbandonMessage(ctx, msg) + err = deadLetterReceiver.AbandonMessage(ctx, msg, &AbandonMessageOptions{ + PropertiesToModify: map[string]interface{}{ + "hello": "world", + }, + }) require.NoError(t, err) msg, err = deadLetterReceiver.receiveMessage(ctx, nil) require.NoError(t, err) require.EqualValues(t, 2, msg.DeliveryCount) + require.EqualValues(t, "world", msg.ApplicationProperties["hello"].(string)) // message from dead letter queue -> Complete -> (deleted from queue) err = deadLetterReceiver.CompleteMessage(ctx, msg) @@ -79,7 +98,11 @@ func TestDeferredMessages(t *testing.T) { // abandon the deferred message, which should return // it to the queue. - err := receiver.AbandonMessage(ctx, msg) + err := receiver.AbandonMessage(ctx, msg, &AbandonMessageOptions{ + PropertiesToModify: map[string]interface{}{ + "hello": "world", + }, + }) require.NoError(t, err) // BUG: we're timing out here, even though our abandon should have put the message @@ -88,6 +111,7 @@ func TestDeferredMessages(t *testing.T) { msg, err = receiver.receiveMessage(ctx, nil) require.NoError(t, err) require.NotNil(t, msg) + require.EqualValues(t, "world", msg.ApplicationProperties["hello"].(string)) }) t.Run("Complete", func(t *testing.T) { @@ -103,12 +127,18 @@ func TestDeferredMessages(t *testing.T) { msg := testStuff.deferMessageForTest(t) // double defer! - err := receiver.DeferMessage(ctx, msg) + err := receiver.DeferMessage(ctx, msg, &DeferMessageOptions{ + PropertiesToModify: map[string]interface{}{ + "hello": "world", + }, + }) require.NoError(t, err) msg, err = receiver.receiveDeferredMessage(ctx, *msg.SequenceNumber) require.NoError(t, err) + require.EqualValues(t, "world", msg.ApplicationProperties["hello"].(string)) + err = receiver.CompleteMessage(ctx, msg) require.NoError(t, err) @@ -169,7 +199,7 @@ func TestMessageSettlementUsingOnlyBackupSettlement(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 1, msg.DeliveryCount) - err = receiver.AbandonMessage(context.Background(), msg) + err = receiver.AbandonMessage(context.Background(), msg, nil) require.NoError(t, err) msg, err = receiver.receiveMessage(ctx, nil) @@ -227,7 +257,7 @@ func newTestStuff(t *testing.T) *testStuff { testStuff.Receiver, err = client.NewReceiverForQueue(queueName, nil) require.NoError(t, err) - testStuff.Sender, err = client.NewSender(queueName) + testStuff.Sender, err = client.NewSender(queueName, nil) require.NoError(t, err) testStuff.DeadLetterReceiver, err = client.NewReceiverForQueue( @@ -256,7 +286,7 @@ func (testStuff *testStuff) deferMessageForTest(t *testing.T) *ReceivedMessage { require.EqualValues(t, 1, msg.DeliveryCount) - err = testStuff.Receiver.DeferMessage(context.Background(), msg) + err = testStuff.Receiver.DeferMessage(context.Background(), msg, nil) require.NoError(t, err) msg, err = testStuff.Receiver.receiveDeferredMessage(context.Background(), *msg.SequenceNumber) diff --git a/sdk/messaging/azservicebus/message_test.go b/sdk/messaging/azservicebus/message_test.go index ec42fe318b2d..d928c7c003a8 100644 --- a/sdk/messaging/azservicebus/message_test.go +++ b/sdk/messaging/azservicebus/message_test.go @@ -25,7 +25,7 @@ func TestMessageUnitTest(t *testing.T) { scheduledEnqueuedTime := time.Now() message = &Message{ - ID: "message id", + MessageID: "message id", Body: []byte("the body"), PartitionKey: to.StringPtr("partition key"), TransactionPartitionKey: to.StringPtr("via partition key"), @@ -132,7 +132,7 @@ func TestAMQPMessageToMessage(t *testing.T) { msg := newReceivedMessage(context.Background(), amqpMsg) - require.EqualValues(t, msg.ID, amqpMsg.Properties.MessageID, "messageID") + require.EqualValues(t, msg.MessageID, amqpMsg.Properties.MessageID, "messageID") require.EqualValues(t, *msg.SessionID, amqpMsg.Properties.GroupID, "groupID") require.EqualValues(t, msg.ContentType, amqpMsg.Properties.ContentType, "contentType") require.EqualValues(t, msg.CorrelationID, amqpMsg.Properties.CorrelationID, "correlation") @@ -141,7 +141,10 @@ func TestAMQPMessageToMessage(t *testing.T) { require.EqualValues(t, *msg.TimeToLive, amqpMsg.Header.TTL, "ttl") require.EqualValues(t, msg.Subject, amqpMsg.Properties.Subject, "subject") require.EqualValues(t, msg.To, amqpMsg.Properties.To, "to") - require.EqualValues(t, msg.Body, amqpMsg.Data[0], "data") + + body, err := msg.Body() + require.NoError(t, err) + require.EqualValues(t, body, amqpMsg.Data[0], "data") expectedAMQPEncodedLockTokenGUID := [16]byte{187, 49, 89, 205, 253, 254, 205, 77, 162, 38, 172, 76, 45, 235, 91, 225} diff --git a/sdk/messaging/azservicebus/migrationguide.md b/sdk/messaging/azservicebus/migrationguide.md index fbc6a3af1e90..e0d7cfb2304d 100644 --- a/sdk/messaging/azservicebus/migrationguide.md +++ b/sdk/messaging/azservicebus/migrationguide.md @@ -1,4 +1,4 @@ -# Guide to migrate from `azure-service-bus-go` to `azservicebus` 0.1.0 +# Guide to migrate from `azure-service-bus-go` to `azservicebus` 0.3.0 This guide is intended to assist in the migration from the pre-release `azure-service-bus-go` package to the latest beta releases (and eventual GA) of the `github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus`. @@ -33,7 +33,7 @@ New (using `azservicebus`): ```go // new code -client, err = azservicebus.NewClientWithConnectionString(connectionString, nil) +client, err = azservicebus.NewClientFromConnectionString(connectionString, nil) ``` ### Sending messages @@ -58,11 +58,26 @@ towards giving the user full control using the `MessageBatch` type. batch, err := sender.NewMessageBatch(ctx, nil) // can be called multiple times -added, err := batch.Add(&azservicebus.Message{ +err := batch.AddMessage(&azservicebus.Message{ Body: []byte("hello world") }) -sender.SendMessage(ctx, batch) +if err != nil { + switch err { + case azservicebus.ErrMessageTooLarge: + // At this point you can do a few things: + // 1. Ignore this message + // 2. Send this batch (it's full) and create a new batch. + // + // The batch can still be used after this error if you have + // smaller messages you'd still like to add in. + fmt.Printf("Failed to add message to batch\n") + default: + exitOnError("Error while trying to add message to batch", err) + } +} + +sender.SendMessageBatch(ctx, batch) ``` ### Processing and receiving messages @@ -152,7 +167,7 @@ Now, using `azservicebus`: // new code // with a Receiver -message, err := receiver.ReceiveMessage(ctx) // or ReceiveMessages() +message, err := receiver.ReceiveMessages(ctx, 10, nil) receiver.CompleteMessage(ctx, message) ``` @@ -167,10 +182,44 @@ credential, err := azidentity.NewDefaultAzureCredential(nil) client, err = azservicebus.NewClient("", credential, nil) ``` +# Entity management using admin.Client + +Administration features, like creating queues, topics and subscriptions, has been moved into a dedicated client (admin.Client). + +```go +adminClient := admin.NewClient() + +// create a queue with default properties +err := adminClient.CreateQueue(context.TODO(), "queue-name", nil, nil) + +// or create a queue and configure some properties +``` + +# Receiving with session entities + +Entities that use sessions can now be be received from: + +```go +// to get a specific session by ID +sessionReceiver, err := client.AcceptSessionForQueue("queue", "session-id", nil) +// or client.AcceptSessionForSubscription + +// to get the next available session from Service Bus (service-assigned) +sessionReceiver, err := client.AcceptNextSessionForQueue("queue", nil) + +// SessionReceiver's are similar to Receiver's with some additional functions: + +// managing session state +sessionData, err := sessionReceiver.GetSessionState(context.TODO()) +err := sessionReceiver.SetSessionState(context.TODO(), []byte("data")) + +// renewing the lock associated with the session +err := sessionReceiver.RenewSessionLock(context.TODO()) +``` + # Upcoming features Some features that are coming in the next beta: -- Management of entities (AdministrationClient) within Service Bus. -- Scheduling and cancellation of messages. -- Sending and receiving to Service Bus session enabled entities. +- Authorization rules +- Topic filters/actions diff --git a/sdk/messaging/azservicebus/processor.go b/sdk/messaging/azservicebus/processor.go index e572df5dadb0..a3ec0683afa2 100644 --- a/sdk/messaging/azservicebus/processor.go +++ b/sdk/messaging/azservicebus/processor.go @@ -260,14 +260,14 @@ func (p *processor) CompleteMessage(ctx context.Context, message *ReceivedMessag // AbandonMessage will cause a message to be returned to the queue or subscription. // This will increment its delivery count, and potentially cause it to be dead lettered // depending on your queue or subscription's configuration. -func (p *processor) AbandonMessage(ctx context.Context, message *ReceivedMessage) error { - return p.settler.AbandonMessage(ctx, message) +func (p *processor) AbandonMessage(ctx context.Context, message *ReceivedMessage, options *AbandonMessageOptions) error { + return p.settler.AbandonMessage(ctx, message, options) } // DeferMessage will cause a message to be deferred. Deferred messages // can be received using `Receiver.ReceiveDeferredMessages`. -func (p *processor) DeferMessage(ctx context.Context, message *ReceivedMessage) error { - return p.settler.DeferMessage(ctx, message) +func (p *processor) DeferMessage(ctx context.Context, message *ReceivedMessage, options *DeferMessageOptions) error { + return p.settler.DeferMessage(ctx, message, options) } // DeadLetterMessage settles a message by moving it to the dead letter queue for a @@ -344,13 +344,13 @@ func (p *processor) processMessage(ctx context.Context, receiver internal.AMQPRe var settleErr error if messageHandlerErr != nil { - settleErr = p.settler.AbandonMessage(ctx, receivedMessage) + settleErr = p.settler.AbandonMessage(ctx, receivedMessage, nil) } else { settleErr = p.settler.CompleteMessage(ctx, receivedMessage) } if settleErr != nil { - p.userErrorHandler(fmt.Errorf("failed to settle message with ID '%s': %w", receivedMessage.ID, settleErr)) + p.userErrorHandler(fmt.Errorf("failed to settle message with ID '%s': %w", receivedMessage.MessageID, settleErr)) return settleErr } } @@ -375,7 +375,7 @@ func checkReceiverMode(receiveMode ReceiveMode) error { if receiveMode == ReceiveModePeekLock || receiveMode == ReceiveModeReceiveAndDelete { return nil } else { - return fmt.Errorf("Invalid receive mode %d, must be either azservicebus.PeekLock or azservicebus.ReceiveAndDelete", receiveMode) + return fmt.Errorf("invalid receive mode %d, must be either azservicebus.PeekLock or azservicebus.ReceiveAndDelete", receiveMode) } } diff --git a/sdk/messaging/azservicebus/processor_test.go b/sdk/messaging/azservicebus/processor_test.go index e7b97ba88540..73e85cb4101c 100644 --- a/sdk/messaging/azservicebus/processor_test.go +++ b/sdk/messaging/azservicebus/processor_test.go @@ -20,7 +20,7 @@ func TestProcessorReceiveWithDefaults(t *testing.T) { defer cleanup() go func() { - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -52,7 +52,10 @@ func TestProcessorReceiveWithDefaults(t *testing.T) { mu.Lock() defer mu.Unlock() - messages = append(messages, string(m.Body)) + body, err := m.Body() + require.NoError(t, err) + + messages = append(messages, string(body)) if len(messages) == 5 { cancel() @@ -90,7 +93,7 @@ func TestProcessorReceiveWith100MessagesWithMaxConcurrency(t *testing.T) { var expectedBodies []string go func() { - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -102,11 +105,10 @@ func TestProcessorReceiveWith100MessagesWithMaxConcurrency(t *testing.T) { // have been sent. for i := 0; i < numMessages; i++ { expectedBodies = append(expectedBodies, fmt.Sprintf("hello world %03d", i)) - added, err := batch.Add(&Message{ + err := batch.AddMessage(&Message{ Body: []byte(expectedBodies[len(expectedBodies)-1]), }) require.NoError(t, err) - require.True(t, added) } require.NoError(t, sender.SendMessageBatch(context.Background(), batch)) @@ -135,7 +137,9 @@ func TestProcessorReceiveWith100MessagesWithMaxConcurrency(t *testing.T) { mu.Lock() defer mu.Unlock() - messages = append(messages, string(m.Body)) + body, err := m.Body() + require.NoError(t, err) + messages = append(messages, string(body)) if len(messages) == 100 { go processor.Close(context.Background()) diff --git a/sdk/messaging/azservicebus/receiver.go b/sdk/messaging/azservicebus/receiver.go index 0655095afa81..3ddcc2cc1825 100644 --- a/sdk/messaging/azservicebus/receiver.go +++ b/sdk/messaging/azservicebus/receiver.go @@ -33,8 +33,6 @@ const ( type SubQueue int const ( - // SubQueueNone means no sub queue. - SubQueueNone SubQueue = 0 // SubQueueDeadLetter targets the dead letter queue for a queue or subscription. SubQueueDeadLetter SubQueue = 1 // SubQueueTransfer targets the transfer dead letter queue for a queue or subscription. @@ -140,9 +138,9 @@ func newReceiver(ns internal.NamespaceWithNewAMQPLinks, entity *entity, cleanupO return receiver, nil } -// ReceiveOptions are options for the ReceiveMessages function. -type ReceiveOptions struct { - maxWaitTimeAfterFirstMessage time.Duration +// ReceiveMessagesOptions are options for the ReceiveMessages function. +type ReceiveMessagesOptions struct { + // For future expansion } // ReceiveMessages receives a fixed number of messages, up to numMessages. @@ -150,7 +148,7 @@ type ReceiveOptions struct { // 1. Cancelling the `ctx` parameter. // 2. An implicit timeout (default: 1 second) that starts after the first // message has been received. -func (r *Receiver) ReceiveMessages(ctx context.Context, maxMessages int, options *ReceiveOptions) ([]*ReceivedMessage, error) { +func (r *Receiver) ReceiveMessages(ctx context.Context, maxMessages int, options *ReceiveMessagesOptions) ([]*ReceivedMessage, error) { r.mu.Lock() isReceiving := r.receiving @@ -278,14 +276,14 @@ func (r *Receiver) CompleteMessage(ctx context.Context, message *ReceivedMessage // AbandonMessage will cause a message to be returned to the queue or subscription. // This will increment its delivery count, and potentially cause it to be dead lettered // depending on your queue or subscription's configuration. -func (r *Receiver) AbandonMessage(ctx context.Context, message *ReceivedMessage) error { - return r.settler.AbandonMessage(ctx, message) +func (r *Receiver) AbandonMessage(ctx context.Context, message *ReceivedMessage, options *AbandonMessageOptions) error { + return r.settler.AbandonMessage(ctx, message, options) } // DeferMessage will cause a message to be deferred. Deferred messages // can be received using `Receiver.ReceiveDeferredMessages`. -func (r *Receiver) DeferMessage(ctx context.Context, message *ReceivedMessage) error { - return r.settler.DeferMessage(ctx, message) +func (r *Receiver) DeferMessage(ctx context.Context, message *ReceivedMessage, options *DeferMessageOptions) error { + return r.settler.DeferMessage(ctx, message, options) } // DeadLetterMessage settles a message by moving it to the dead letter queue for a @@ -311,7 +309,7 @@ func (r *Receiver) receiveDeferredMessage(ctx context.Context, sequenceNumber in } // receiveMessage receives a single message, waiting up to `ReceiveOptions.MaxWaitTime` (default: 60 seconds) -func (r *Receiver) receiveMessage(ctx context.Context, options *ReceiveOptions) (*ReceivedMessage, error) { +func (r *Receiver) receiveMessage(ctx context.Context, options *ReceiveMessagesOptions) (*ReceivedMessage, error) { messages, err := r.ReceiveMessages(ctx, 1, options) if err != nil { @@ -325,7 +323,7 @@ func (r *Receiver) receiveMessage(ctx context.Context, options *ReceiveOptions) return messages[0], nil } -func (r *Receiver) receiveMessagesImpl(ctx context.Context, maxMessages int, options *ReceiveOptions) ([]*ReceivedMessage, error) { +func (r *Receiver) receiveMessagesImpl(ctx context.Context, maxMessages int, options *ReceiveMessagesOptions) ([]*ReceivedMessage, error) { // There are three phases for this function: // Phase 1. // Phase 2. @@ -333,16 +331,6 @@ func (r *Receiver) receiveMessagesImpl(ctx context.Context, maxMessages int, opt // user isn't actually waiting for anymore. So we make sure that #3 runs if the // link is still valid. // Phase 3. - localOpts := &ReceiveOptions{ - maxWaitTimeAfterFirstMessage: time.Second, - } - - if options != nil { - if options.maxWaitTimeAfterFirstMessage != 0 { - localOpts.maxWaitTimeAfterFirstMessage = options.maxWaitTimeAfterFirstMessage - } - } - _, receiver, _, linksRevision, err := r.amqpLinks.Get(ctx) if err != nil { @@ -358,7 +346,13 @@ func (r *Receiver) receiveMessagesImpl(ctx context.Context, maxMessages int, opt return nil, err } - messages, err := r.getMessages(ctx, receiver, maxMessages, localOpts) + defaultTimeAfterFirstMessage := 20 * time.Millisecond + + if r.receiveMode == ReceiveModeReceiveAndDelete { + defaultTimeAfterFirstMessage = time.Second + } + + messages, err := r.getMessages(ctx, receiver, maxMessages, defaultTimeAfterFirstMessage) if err != nil { return nil, err @@ -369,38 +363,35 @@ func (r *Receiver) receiveMessagesImpl(ctx context.Context, maxMessages int, opt return messages, nil } - return r.drainLink(receiver, messages) + return r.drainLink(ctx, receiver, messages) } // drainLink initiates a drainLink on the link. Service Bus will send whatever messages it might have still had and // set our link credit to 0. -func (r *Receiver) drainLink(receiver internal.AMQPReceiver, messages []*ReceivedMessage) ([]*ReceivedMessage, error) { - receiveCtx, cancelReceive := context.WithCancel(context.Background()) - +// ctxForLoggingOnly is literally only used for when we need to extract context for logging. This function will always attempt +// to complete, ignoring cancellation, otherwise we can leave the link with messages that haven't been returned to the user. +func (r *Receiver) drainLink(ctxForLoggingOnly context.Context, receiver internal.AMQPReceiver, messages []*ReceivedMessage) ([]*ReceivedMessage, error) { // start the drain asynchronously. Note that we ignore the user's context at this point // since draining makes sure we don't get messages when nobody is receiving. - go func() { - if err := receiver.DrainCredit(context.Background()); err != nil { - tab.For(receiveCtx).Debug(fmt.Sprintf("Draining of credit failed. link will be closed and will re-open on next receive: %s", err.Error())) + if err := receiver.DrainCredit(context.Background()); err != nil { + tab.For(ctxForLoggingOnly).Debug(fmt.Sprintf("Draining of credit failed. link will be closed and will re-open on next receive: %s", err.Error())) - // if the drain fails we just close the link so it'll re-open at the next receive. - if err := r.amqpLinks.Close(context.Background(), false); err != nil { - tab.For(receiveCtx).Debug(fmt.Sprintf("Failed to close links on ReceiveMessages cleanup. Not fatal: %s", err.Error())) - } + // if the drain fails we just close the link so it'll re-open at the next receive. + if err := r.amqpLinks.Close(context.Background(), false); err != nil { + tab.For(ctxForLoggingOnly).Debug(fmt.Sprintf("Failed to close links on ReceiveMessages cleanup. Not fatal: %s", err.Error())) } - cancelReceive() - }() + } - // Receive until the drain completes, at which point it'll cancel - // our context. - // NOTE: That's a gap here where we need to be able to drain _only_ the internally cached messages - // in the receiver. Filed as https://github.com/Azure/go-amqp/issues/71 + // Draining data from the receiver's prefetched queue. This won't wait for new messages to + // arrive, so it'll only receive messages that arrived prior to the drain. for { - am, err := receiver.Receive(receiveCtx) + am, err := receiver.Prefetched(context.Background()) - if internal.IsCancelError(err) { + if am == nil || internal.IsCancelError(err) { break - } else if err != nil { + } + + if err != nil { // something fatal happened, we will just _ = r.amqpLinks.Close(context.TODO(), false) @@ -411,7 +402,7 @@ func (r *Receiver) drainLink(receiver internal.AMQPReceiver, messages []*Receive } } - messages = append(messages, newReceivedMessage(receiveCtx, am)) + messages = append(messages, newReceivedMessage(ctxForLoggingOnly, am)) } return messages, nil @@ -419,7 +410,7 @@ func (r *Receiver) drainLink(receiver internal.AMQPReceiver, messages []*Receive // getMessages receives messages until a link failure, timeout or the user // cancels their context. -func (r *Receiver) getMessages(ctx context.Context, receiver internal.AMQPReceiver, maxMessages int, ropts *ReceiveOptions) ([]*ReceivedMessage, error) { +func (r *Receiver) getMessages(ctx context.Context, receiver internal.AMQPReceiver, maxMessages int, maxWaitTimeAfterFirstMessage time.Duration) ([]*ReceivedMessage, error) { var messages []*ReceivedMessage for { @@ -448,7 +439,7 @@ func (r *Receiver) getMessages(ctx context.Context, receiver internal.AMQPReceiv if len(messages) == 1 { var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, time.Second) + ctx, cancel = context.WithTimeout(ctx, maxWaitTimeAfterFirstMessage) defer cancel() } } @@ -482,7 +473,7 @@ func (e *entity) String() (string, error) { } func (e *entity) SetSubQueue(subQueue SubQueue) error { - if subQueue == SubQueueNone { + if subQueue == 0 { return nil } else if subQueue == SubQueueDeadLetter || subQueue == SubQueueTransfer { e.subqueue = subQueue diff --git a/sdk/messaging/azservicebus/receiver_test.go b/sdk/messaging/azservicebus/receiver_test.go index c7f7bae500da..13966594c57e 100644 --- a/sdk/messaging/azservicebus/receiver_test.go +++ b/sdk/messaging/azservicebus/receiver_test.go @@ -20,7 +20,7 @@ func TestReceiverSendFiveReceiveFive(t *testing.T) { serviceBusClient, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -42,9 +42,12 @@ func TestReceiverSendFiveReceiveFive(t *testing.T) { require.EqualValues(t, 5, len(messages)) for i := 0; i < 5; i++ { + body, err := messages[i].Body() + require.NoError(t, err) + require.EqualValues(t, fmt.Sprintf("[%d]: send five, receive five", i), - string(messages[i].Body)) + string(body)) require.NoError(t, receiver.CompleteMessage(context.Background(), messages[i])) } @@ -54,7 +57,7 @@ func TestReceiverForceTimeoutWithTooFewMessages(t *testing.T) { serviceBusClient, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -83,7 +86,7 @@ func TestReceiverAbandon(t *testing.T) { serviceBusClient, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -100,7 +103,7 @@ func TestReceiverAbandon(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 1, len(messages)) - require.NoError(t, receiver.AbandonMessage(context.Background(), messages[0])) + require.NoError(t, receiver.AbandonMessage(context.Background(), messages[0], nil)) abandonedMessages, err := receiver.ReceiveMessages(context.Background(), 1, nil) require.NoError(t, err) @@ -115,7 +118,7 @@ func TestReceiveWithEarlyFirstMessageTimeout(t *testing.T) { serviceBusClient, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -132,11 +135,7 @@ func TestReceiveWithEarlyFirstMessageTimeout(t *testing.T) { defer cancel() startTime := time.Now() - messages, err := receiver.ReceiveMessages(ctx, 1, - &ReceiveOptions{ - maxWaitTimeAfterFirstMessage: time.Millisecond, - }) - + messages, err := receiver.ReceiveMessages(ctx, 1, nil) require.NoError(t, err) require.EqualValues(t, 1, len(messages)) @@ -148,7 +147,7 @@ func TestReceiverSendAndReceiveManyTimes(t *testing.T) { serviceBusClient, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -187,7 +186,7 @@ func TestReceiverDeferAndReceiveDeferredMessages(t *testing.T) { client, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) ctx := context.TODO() @@ -208,7 +207,7 @@ func TestReceiverDeferAndReceiveDeferredMessages(t *testing.T) { var sequenceNumbers []int64 for _, m := range messages { - err = receiver.DeferMessage(ctx, m) + err = receiver.DeferMessage(ctx, m, nil) require.NoError(t, err) sequenceNumbers = append(sequenceNumbers, *m.SequenceNumber) @@ -230,7 +229,7 @@ func TestReceiverPeek(t *testing.T) { serviceBusClient, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := serviceBusClient.NewSender(queueName) + sender, err := serviceBusClient.NewSender(queueName, nil) require.NoError(t, err) ctx := context.TODO() @@ -241,12 +240,11 @@ func TestReceiverPeek(t *testing.T) { require.NoError(t, err) for i := 0; i < 3; i++ { - added, err := batch.Add(&Message{ + err := batch.AddMessage(&Message{ Body: []byte(fmt.Sprintf("Message %d", i)), }) require.NoError(t, err) - require.True(t, added) } err = sender.SendMessageBatch(ctx, batch) @@ -261,7 +259,7 @@ func TestReceiverPeek(t *testing.T) { // put them all back for _, m := range messages { - require.NoError(t, receiver.AbandonMessage(ctx, m)) + require.NoError(t, receiver.AbandonMessage(ctx, m, nil)) } peekedMessages, err := receiver.PeekMessages(ctx, 2, nil) @@ -284,8 +282,11 @@ func TestReceiverPeek(t *testing.T) { require.NoError(t, err) require.EqualValues(t, 1, len(repeekedMessages)) + body, err := peekedMessages2[0].Body() + require.NoError(t, err) + require.EqualValues(t, []string{ - string(peekedMessages2[0].Body), + string(body), }, getSortedBodies(repeekedMessages)) // and peek again (note it won't reset so there'll be "nothing") @@ -298,7 +299,7 @@ func TestReceiver_RenewMessageLock(t *testing.T) { client, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) err = sender.SendMessage(context.Background(), &Message{ @@ -397,7 +398,19 @@ func (messages receivedMessageSlice) Len() int { } func (messages receivedMessageSlice) Less(i, j int) bool { - return string(messages[i].Body) < string(messages[j].Body) + bodyI, err := messages[i].Body() + + if err != nil { + panic(err) + } + + bodyJ, err := messages[j].Body() + + if err != nil { + panic(err) + } + + return string(bodyI) < string(bodyJ) } func (messages receivedMessageSlice) Swap(i, j int) { diff --git a/sdk/messaging/azservicebus/sender.go b/sdk/messaging/azservicebus/sender.go index bc63c5fcedd7..763055e0eb99 100644 --- a/sdk/messaging/azservicebus/sender.go +++ b/sdk/messaging/azservicebus/sender.go @@ -5,8 +5,6 @@ package azservicebus import ( "context" - "errors" - "fmt" "time" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal" @@ -22,25 +20,19 @@ type ( cleanupOnClose func() links internal.AMQPLinks } - - // SendableMessage can be sent using `Sender.SendMessage` or `Sender.SendMessages`. - // Message implements this interface. - SendableMessage interface { - toAMQPMessage() *amqp.Message - messageType() string - } ) // tracing const ( - spanNameSendMessageFmt string = "sb.sender.SendMessage.%s" + spanNameSendMessage string = "sb.sender.SendMessage" + spanNameSendBatch string = "sb.sender.SendBatch" ) // MessageBatchOptions contains options for the `Sender.NewMessageBatch` function. type MessageBatchOptions struct { - // MaxSizeInBytes overrides the max size (in bytes) for a batch. + // MaxBytes overrides the max size (in bytes) for a batch. // By default NewMessageBatch will use the max message size provided by the service. - MaxSizeInBytes int + MaxBytes int32 } // NewMessageBatch can be used to create a batch that contain multiple @@ -53,18 +45,18 @@ func (s *Sender) NewMessageBatch(ctx context.Context, options *MessageBatchOptio return nil, err } - maxBytes := int(sender.MaxMessageSize()) + maxBytes := int32(sender.MaxMessageSize()) - if options != nil && options.MaxSizeInBytes != 0 { - maxBytes = options.MaxSizeInBytes + if options != nil && options.MaxBytes != 0 { + maxBytes = options.MaxBytes } return &MessageBatch{maxBytes: maxBytes}, nil } -// SendMessage sends a SendableMessage (Message) to a queue or topic. -func (s *Sender) SendMessage(ctx context.Context, message SendableMessage) error { - ctx, span := s.startProducerSpanFromContext(ctx, fmt.Sprintf(spanNameSendMessageFmt, message.messageType())) +// SendMessage sends a Message to a queue or topic. +func (s *Sender) SendMessage(ctx context.Context, message *Message) error { + ctx, span := s.startProducerSpanFromContext(ctx, spanNameSendMessage) defer span.End() sender, _, _, _, err := s.links.Get(ctx) @@ -79,7 +71,7 @@ func (s *Sender) SendMessage(ctx context.Context, message SendableMessage) error // SendMessageBatch sends a MessageBatch to a queue or topic. // Message batches can be created using `Sender.NewMessageBatch`. func (s *Sender) SendMessageBatch(ctx context.Context, batch *MessageBatch) error { - ctx, span := s.startProducerSpanFromContext(ctx, fmt.Sprintf(spanNameSendMessageFmt, "batch")) + ctx, span := s.startProducerSpanFromContext(ctx, spanNameSendBatch) defer span.End() sender, _, _, _, err := s.links.Get(ctx) @@ -91,74 +83,20 @@ func (s *Sender) SendMessageBatch(ctx context.Context, batch *MessageBatch) erro return sender.Send(ctx, batch.toAMQPMessage()) } -// SendMessages sends messages to a queue or topic, using a single MessageBatch. -// If the messages cannot fit into a single MessageBatch this function will fail. -func (s *Sender) SendMessages(ctx context.Context, messages []*Message) error { - batch, err := s.NewMessageBatch(ctx, nil) - - if err != nil { - return err - } - - for _, m := range messages { - added, err := batch.Add(m) - - if err != nil { - return err - } - - if !added { - // to avoid partial failure scenarios we just bail if the messages are too large to fit - // into a single batch. - return errors.New("Messages were too big to fit in a single batch. Remove some messages and try again or create your own batch using Sender.NewMessageBatch(), which gives more fine-grained control.") - } - } - - return s.SendMessageBatch(ctx, batch) -} - -// ScheduleMessage schedules a message to appear on Service Bus Queue/Subscription at a later time. -// Returns the sequence number of the message that was scheduled. If the message hasn't been -// delivered you can cancel using `Receiver.CancelScheduleMessage(s)` -func (s *Sender) ScheduleMessage(ctx context.Context, message SendableMessage, scheduledEnqueueTime time.Time) (int64, error) { - sequenceNumbers, err := s.ScheduleMessages(ctx, []SendableMessage{message}, scheduledEnqueueTime) - - if err != nil { - return 0, err - } - - return sequenceNumbers[0], nil -} - -// ScheduleMessages schedules a slice of messages to appear on Service Bus Queue/Subscription at a later time. +// ScheduleMessages schedules a slice of Messages to appear on Service Bus Queue/Subscription at a later time. // Returns the sequence numbers of the messages that were scheduled. Messages that haven't been // delivered can be cancelled using `Receiver.CancelScheduleMessage(s)` -func (s *Sender) ScheduleMessages(ctx context.Context, messages []SendableMessage, scheduledEnqueueTime time.Time) ([]int64, error) { - _, _, mgmt, _, err := s.links.Get(ctx) - - if err != nil { - return nil, err - } - +func (s *Sender) ScheduleMessages(ctx context.Context, messages []*Message, scheduledEnqueueTime time.Time) ([]int64, error) { var amqpMessages []*amqp.Message for _, m := range messages { amqpMessages = append(amqpMessages, m.toAMQPMessage()) } - return mgmt.ScheduleMessages(ctx, scheduledEnqueueTime, amqpMessages...) + return s.scheduleAMQPMessages(ctx, amqpMessages, scheduledEnqueueTime) } -// CancelScheduledMessage cancels a message that was scheduled. -func (s *Sender) CancelScheduledMessage(ctx context.Context, sequenceNumber int64) error { - _, _, mgmt, _, err := s.links.Get(ctx) - - if err != nil { - return err - } - - return mgmt.CancelScheduled(ctx, sequenceNumber) -} +// MessageBatch changes // CancelScheduledMessages cancels multiple messages that were scheduled. func (s *Sender) CancelScheduledMessages(ctx context.Context, sequenceNumber []int64) error { @@ -177,6 +115,16 @@ func (s *Sender) Close(ctx context.Context) error { return s.links.Close(ctx, true) } +func (s *Sender) scheduleAMQPMessages(ctx context.Context, messages []*amqp.Message, scheduledEnqueueTime time.Time) ([]int64, error) { + _, _, mgmt, _, err := s.links.Get(ctx) + + if err != nil { + return nil, err + } + + return mgmt.ScheduleMessages(ctx, scheduledEnqueueTime, messages...) +} + func (sender *Sender) createSenderLink(ctx context.Context, session internal.AMQPSession) (internal.AMQPSenderCloser, internal.AMQPReceiverCloser, error) { amqpSender, err := session.NewSender( amqp.LinkSenderSettle(amqp.ModeMixed), diff --git a/sdk/messaging/azservicebus/sender_test.go b/sdk/messaging/azservicebus/sender_test.go index 69456d277830..09e3b3380738 100644 --- a/sdk/messaging/azservicebus/sender_test.go +++ b/sdk/messaging/azservicebus/sender_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" "github.com/stretchr/testify/require" ) @@ -19,7 +20,7 @@ func Test_Sender_SendBatchOfTwo(t *testing.T) { ctx := context.Background() - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(ctx) @@ -27,17 +28,15 @@ func Test_Sender_SendBatchOfTwo(t *testing.T) { batch, err := sender.NewMessageBatch(ctx, nil) require.NoError(t, err) - added, err := batch.Add(&Message{ + err = batch.AddMessage(&Message{ Body: []byte("[0] message in batch"), }) require.NoError(t, err) - require.True(t, added) - added, err = batch.Add(&Message{ + err = batch.AddMessage(&Message{ Body: []byte("[1] message in batch"), }) require.NoError(t, err) - require.True(t, added) err = sender.SendMessageBatch(ctx, batch) require.NoError(t, err) @@ -47,19 +46,43 @@ func Test_Sender_SendBatchOfTwo(t *testing.T) { require.NoError(t, err) defer receiver.Close(ctx) - messages, err := receiver.ReceiveMessages(ctx, 2, nil) + messages := receiveAll(t, receiver, 2) require.NoError(t, err) require.EqualValues(t, []string{"[0] message in batch", "[1] message in batch"}, getSortedBodies(messages)) } +func receiveAll(t *testing.T, receiver *Receiver, expected int) []*ReceivedMessage { + var all []*ReceivedMessage + + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute)) + defer cancel() + + for { + messages, err := receiver.ReceiveMessages(ctx, 1+2, nil) + require.NoError(t, err) + + if len(messages) == 0 { + break + } + + all = append(all, messages...) + + if len(all) == expected { + break + } + } + + return all +} + func Test_Sender_UsingPartitionedQueue(t *testing.T) { - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ EnablePartitioning: to.BoolPtr(true), }) defer cleanup() - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -68,36 +91,32 @@ func Test_Sender_UsingPartitionedQueue(t *testing.T) { require.NoError(t, err) defer receiver.Close(context.Background()) - err = sender.SendMessage(context.Background(), &Message{ - ID: "message ID", - Body: []byte("1. single partitioned message"), - PartitionKey: to.StringPtr("partitionKey1"), - }) - require.NoError(t, err) - batch, err := sender.NewMessageBatch(context.Background(), nil) require.NoError(t, err) - added, err := batch.Add(&Message{ + err = batch.AddMessage(&Message{ Body: []byte("2. Message in batch"), PartitionKey: to.StringPtr("partitionKey1"), }) require.NoError(t, err) - require.True(t, added) - added, err = batch.Add(&Message{ + err = batch.AddMessage(&Message{ Body: []byte("3. Message in batch"), PartitionKey: to.StringPtr("partitionKey1"), }) require.NoError(t, err) - require.True(t, added) err = sender.SendMessageBatch(context.Background(), batch) require.NoError(t, err) - messages, err := receiver.ReceiveMessages(context.Background(), 1+2, nil) + err = sender.SendMessage(context.Background(), &Message{ + MessageID: "message ID", + Body: []byte("1. single partitioned message"), + PartitionKey: to.StringPtr("partitionKey1"), + }) require.NoError(t, err) + messages := receiveAll(t, receiver, 3) sort.Sort(receivedMessages(messages)) require.EqualValues(t, 3, len(messages)) @@ -107,47 +126,13 @@ func Test_Sender_UsingPartitionedQueue(t *testing.T) { require.EqualValues(t, "partitionKey1", *messages[2].PartitionKey) } -func Test_Sender_SendMessages(t *testing.T) { - ctx := context.Background() - - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ - EnablePartitioning: to.BoolPtr(true), - }) - defer cleanup() - - receiver, err := client.NewReceiverForQueue( - queueName, &ReceiverOptions{ReceiveMode: ReceiveModeReceiveAndDelete}) - require.NoError(t, err) - defer receiver.Close(context.Background()) - - sender, err := client.NewSender(queueName) - require.NoError(t, err) - defer sender.Close(context.Background()) - - err = sender.SendMessages(ctx, []*Message{ - { - Body: []byte("hello"), - }, - { - Body: []byte("world"), - }, - }) - - require.NoError(t, err) - - messages, err := receiver.ReceiveMessages(ctx, 2, nil) - require.NoError(t, err) - - require.EqualValues(t, []string{"hello", "world"}, getSortedBodies(messages)) -} - func Test_Sender_SendMessages_resend(t *testing.T) { client, cleanup, queueName := setupLiveTest(t, nil) defer cleanup() ctx := context.Background() - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) peekLockReceiver, err := client.NewReceiverForQueue(queueName, &ReceiverOptions{ @@ -209,7 +194,7 @@ func Test_Sender_ScheduleMessages(t *testing.T) { require.NoError(t, err) defer receiver.Close(context.Background()) - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) defer sender.Close(context.Background()) @@ -222,9 +207,9 @@ func Test_Sender_ScheduleMessages(t *testing.T) { // `Scheduled` sequenceNumbers, err := sender.ScheduleMessages(ctx, - []SendableMessage{ - &Message{Body: []byte("To the future (that will be cancelled!)")}, - &Message{Body: []byte("To the future (not cancelled)")}, + []*Message{ + {Body: []byte("To the future (that will be cancelled!)")}, + {Body: []byte("To the future (not cancelled)")}, }, nearFuture) @@ -264,7 +249,13 @@ func getSortedBodies(messages []*ReceivedMessage) []string { var bodies []string for _, msg := range messages { - bodies = append(bodies, string(msg.Body)) + body, err := msg.Body() + + if err != nil { + panic(err) + } + + bodies = append(bodies, string(body)) } return bodies @@ -278,7 +269,19 @@ func (rm receivedMessages) Len() int { // Less compares the messages assuming the .Body field is a valid string. func (rm receivedMessages) Less(i, j int) bool { - return string(rm[i].Body) < string(rm[j].Body) + bodyI, err := rm[i].Body() + + if err != nil { + panic(err) + } + + bodyJ, err := rm[j].Body() + + if err != nil { + panic(err) + } + + return string(bodyI) < string(bodyJ) } func (rm receivedMessages) Swap(i, j int) { diff --git a/sdk/messaging/azservicebus/session_receiver.go b/sdk/messaging/azservicebus/session_receiver.go index c8c8d3cf3111..95517483734d 100644 --- a/sdk/messaging/azservicebus/session_receiver.go +++ b/sdk/messaging/azservicebus/session_receiver.go @@ -105,7 +105,7 @@ func newSessionReceiver(ctx context.Context, sessionID *string, ns internal.Name // 1. Cancelling the `ctx` parameter. // 2. An implicit timeout (default: 1 second) that starts after the first // message has been received. -func (r *SessionReceiver) ReceiveMessages(ctx context.Context, maxMessages int, options *ReceiveOptions) ([]*ReceivedMessage, error) { +func (r *SessionReceiver) ReceiveMessages(ctx context.Context, maxMessages int, options *ReceiveMessagesOptions) ([]*ReceivedMessage, error) { return r.inner.ReceiveMessages(ctx, maxMessages, options) } @@ -140,14 +140,14 @@ func (r *SessionReceiver) CompleteMessage(ctx context.Context, message *Received // AbandonMessage will cause a message to be returned to the queue or subscription. // This will increment its delivery count, and potentially cause it to be dead lettered // depending on your queue or subscription's configuration. -func (r *SessionReceiver) AbandonMessage(ctx context.Context, message *ReceivedMessage) error { - return r.inner.AbandonMessage(ctx, message) +func (r *SessionReceiver) AbandonMessage(ctx context.Context, message *ReceivedMessage, options *AbandonMessageOptions) error { + return r.inner.AbandonMessage(ctx, message, options) } // DeferMessage will cause a message to be deferred. Deferred messages // can be received using `Receiver.ReceiveDeferredMessages`. -func (r *SessionReceiver) DeferMessage(ctx context.Context, message *ReceivedMessage) error { - return r.inner.DeferMessage(ctx, message) +func (r *SessionReceiver) DeferMessage(ctx context.Context, message *ReceivedMessage, options *DeferMessageOptions) error { + return r.inner.DeferMessage(ctx, message, options) } // DeadLetterMessage settles a message by moving it to the dead letter queue for a diff --git a/sdk/messaging/azservicebus/session_receiver_test.go b/sdk/messaging/azservicebus/session_receiver_test.go index aff94913d66d..cc1f4956ce5b 100644 --- a/sdk/messaging/azservicebus/session_receiver_test.go +++ b/sdk/messaging/azservicebus/session_receiver_test.go @@ -9,13 +9,14 @@ import ( "testing" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/admin" "github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus/internal" "github.com/Azure/go-amqp" "github.com/stretchr/testify/require" ) func TestSessionReceiver_acceptSession(t *testing.T) { - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ RequiresSession: to.BoolPtr(true), }) defer cleanup() @@ -23,7 +24,7 @@ func TestSessionReceiver_acceptSession(t *testing.T) { ctx := context.Background() // send a message to a specific session - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) err = sender.SendMessage(ctx, &Message{ @@ -38,7 +39,10 @@ func TestSessionReceiver_acceptSession(t *testing.T) { msg, err := receiver.inner.receiveMessage(ctx, nil) require.NoError(t, err) - require.EqualValues(t, "session-based message", msg.Body) + body, err := msg.Body() + require.NoError(t, err) + + require.EqualValues(t, "session-based message", body) require.EqualValues(t, "session-1", *msg.SessionID) require.NoError(t, receiver.CompleteMessage(ctx, msg)) @@ -58,7 +62,7 @@ func TestSessionReceiver_blankSessionIDs(t *testing.T) { t.Skip("Can't run blank session ID test because of issue") // errors while closing links: amqp sender close error: *Error{Condition: amqp:not-allowed, Description: The SessionId was not set on a message, and it cannot be sent to the entity. Entities that have session support enabled can only receive messages that have the SessionId set to a valid value. - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ RequiresSession: to.BoolPtr(true), }) defer cleanup() @@ -66,7 +70,7 @@ func TestSessionReceiver_blankSessionIDs(t *testing.T) { ctx := context.Background() // send a message to a specific session - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) err = sender.SendMessage(ctx, &Message{ @@ -89,7 +93,7 @@ func TestSessionReceiver_blankSessionIDs(t *testing.T) { } func TestSessionReceiver_acceptSessionButAlreadyLocked(t *testing.T) { - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ RequiresSession: to.BoolPtr(true), }) defer cleanup() @@ -108,14 +112,14 @@ func TestSessionReceiver_acceptSessionButAlreadyLocked(t *testing.T) { } func TestSessionReceiver_acceptNextSession(t *testing.T) { - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ RequiresSession: to.BoolPtr(true), }) defer cleanup() ctx := context.Background() - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) err = sender.SendMessage(ctx, &Message{ @@ -132,7 +136,9 @@ func TestSessionReceiver_acceptNextSession(t *testing.T) { msg, err := receiver.inner.receiveMessage(ctx, nil) require.NoError(t, err) - require.EqualValues(t, "session-based message", msg.Body) + body, err := msg.Body() + require.NoError(t, err) + require.EqualValues(t, "session-based message", body) require.EqualValues(t, "acceptnextsession-test", *msg.SessionID) require.NoError(t, receiver.CompleteMessage(ctx, msg)) @@ -151,7 +157,7 @@ func TestSessionReceiver_acceptNextSession(t *testing.T) { func TestSessionReceiver_noSessionsAvailable(t *testing.T) { t.Skip("Really slow test (since it has to wait for a timeout from the service)") - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ RequiresSession: to.BoolPtr(true), }) defer cleanup() @@ -174,7 +180,7 @@ func TestSessionReceiver_noSessionsAvailable(t *testing.T) { } func TestSessionReceiver_nonSessionReceiver(t *testing.T) { - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ RequiresSession: to.BoolPtr(true), }) defer cleanup() @@ -202,7 +208,7 @@ func TestSessionReceiver_nonSessionReceiver(t *testing.T) { } func TestSessionReceiver_RenewSessionLock(t *testing.T) { - client, cleanup, queueName := setupLiveTest(t, &QueueProperties{ + client, cleanup, queueName := setupLiveTest(t, &admin.QueueProperties{ RequiresSession: to.BoolPtr(true), }) defer cleanup() @@ -210,7 +216,7 @@ func TestSessionReceiver_RenewSessionLock(t *testing.T) { sessionReceiver, err := client.AcceptSessionForQueue(context.Background(), queueName, "session-1", nil) require.NoError(t, err) - sender, err := client.NewSender(queueName) + sender, err := client.NewSender(queueName, nil) require.NoError(t, err) err = sender.SendMessage(context.Background(), &Message{