Skip to content

Commit

Permalink
feat: support for detaching subscriptions (#1032)
Browse files Browse the repository at this point in the history
* feat: add porcelain support for detaching subscriptions

* docs: update comment for createSubscription test

* samples: add detach subscription sample

* docs: update doc links for subscription detach

* feat: add detached() method as an easy shortcut for grabbing the 'detached' metadata from a subscription

* docs: update the new detach subscription sample to use the better detached() call (and a few CR comments)

* tests: add a sample system-test for testing the detach subscriptions sample

* fix: properly handle alternate API endpoints (not all of which are emulators)

* feat: move detach methods into the main PubSub object

* tests: don't try to system-test the detach sample yet

* feat: remove the detach call from Topic since it doesn't need any topic-related info

* fix: allow multiple trailing slashes in API endpoint again

* fix: revert alterate API endpoint changes, to put into another PR

* docs: use arrow functions for examples

* tests: re-enable the detach subscription test

* tests: add missing unit tests for the newly added library bits

* tests: add a system-test for the subscription detach methods
  • Loading branch information
feywind authored Jul 20, 2020
1 parent 5650e05 commit c5af3a9
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 2 deletions.
4 changes: 2 additions & 2 deletions samples/createSubscription.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
// limitations under the License.

/**
* This application demonstrates how to perform basic operations on
* subscriptions with the Google Cloud Pub/Sub API.
* This sample demonstrates how to create subscriptions with the
* Google Cloud Pub/Sub API.
*
* For more information, see the README.md under /pubsub and the documentation
* at https://cloud.google.com/pubsub/docs.
Expand Down
68 changes: 68 additions & 0 deletions samples/detachSubscription.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* This sample demonstrates how to detach subscriptions with the
* Google Cloud Pub/Sub API.
*
* For more information, see the README.md under /pubsub and the documentation
* at https://cloud.google.com/pubsub/docs.
*/

'use strict';

// sample-metadata:
// title: Detach Subscription
// description: Detaches a subscription from a topic.
// usage: node detachSubscription.js <existing-subscription-name>

function main(subscriptionName = 'YOUR_EXISTING_SUBSCRIPTION_NAME') {
// [START pubsub_detach_subscription]
/**
* TODO(developer): Uncomment these variables before running the sample.
*/
// const subscriptionName = 'YOUR_EXISTING_SUBSCRIPTION_NAME';

// Imports the Google Cloud client library
const {PubSub} = require('@google-cloud/pubsub');

// Creates a client; cache this for further use
const pubSubClient = new PubSub();

async function detachSubscription() {
// Gets the status of the existing subscription
const sub = pubSubClient.subscription(subscriptionName);
const [detached] = await sub.detached();
console.log(
`Subscription ${subscriptionName} 'before' detached status: ${detached}`
);

await pubSubClient.detachSubscription(subscriptionName);
console.log(`Subscription ${subscriptionName} detach request was sent.`);

const [updatedDetached] = await sub.detached();
console.log(
`Subscription ${subscriptionName} 'after' detached status: ${updatedDetached}`
);
}

detachSubscription();
// [END pubsub_detach_subscription]
}

process.on('unhandledRejection', err => {
console.error(err.message);
process.exitCode = 1;
});
main(...process.argv.slice(2));
14 changes: 14 additions & 0 deletions samples/system-test/subscriptions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ describe('subscriptions', () => {
const subscriptionNameFive = `sub5-${runId}`;
const subscriptionNameSix = `sub6-${runId}`;
const subscriptionNameSeven = `sub7-${runId}`;
const subscriptionNameDetach = `testdetachsubsxyz-${runId}`;
const fullTopicNameOne = `projects/${projectId}/topics/${topicNameOne}`;
const fullSubscriptionNameOne = `projects/${projectId}/subscriptions/${subscriptionNameOne}`;
const fullSubscriptionNameTwo = `projects/${projectId}/subscriptions/${subscriptionNameTwo}`;
Expand Down Expand Up @@ -291,6 +292,19 @@ describe('subscriptions', () => {
assert(subscriptions.every(s => s.name !== fullSubscriptionNameOne));
});

it('should detach a subscription', async () => {
await pubsub.createSubscription(topicNameOne, subscriptionNameDetach);
const output = execSync(
`${commandFor('detachSubscription')} ${subscriptionNameDetach}`
);
assert.include(output, "'before' detached status: false");
assert.include(output, "'after' detached status: true");
const [subscriptionDetached] = await pubsub
.subscription(subscriptionNameDetach)
.detached();
assert(subscriptionDetached === true);
});

it('should create a subscription with dead letter policy.', async () => {
const output = execSync(
`${commandFor(
Expand Down
72 changes: 72 additions & 0 deletions src/pubsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
CreateSubscriptionOptions,
CreateSubscriptionCallback,
CreateSubscriptionResponse,
DetachSubscriptionCallback,
DetachSubscriptionResponse,
} from './subscription';
import {
Topic,
Expand Down Expand Up @@ -116,6 +118,9 @@ export type EmptyResponse = [google.protobuf.IEmpty];
export type ExistsCallback = RequestCallback<boolean>;
export type ExistsResponse = [boolean];

export type DetachedCallback = RequestCallback<boolean>;
export type DetachedResponse = [boolean];

export interface GetClientConfig {
client: 'PublisherClient' | 'SubscriberClient';
}
Expand Down Expand Up @@ -541,6 +546,73 @@ export class PubSub {
}
);
}

detachSubscription(
name: string,
gaxOpts?: CallOptions
): Promise<DetachSubscriptionResponse>;
detachSubscription(name: string, callback: DetachSubscriptionCallback): void;
detachSubscription(
name: string,
gaxOpts: CallOptions,
callback: DetachSubscriptionCallback
): void;
/**
* Detach a subscription with the given name.
*
* @see [Admin: Pub/Sub administration API Documentation]{@link https://cloud.google.com/pubsub/docs/admin}
*
* @param {string} name Name of the subscription.
* @param {object} [gaxOpts] Request configuration options, outlined
* here: https://googleapis.github.io/gax-nodejs/interfaces/CallOptions.html.
* @param {DetachSubscriptionCallback} [callback] Callback function.
* @returns {Promise<DetachSubscriptionResponse>}
*
* @example
* const {PubSub} = require('@google-cloud/pubsub');
* const pubsub = new PubSub();
*
* pubsub.detachSubscription('my-sub', (err, topic, apiResponse) => {
* if (!err) {
* // The topic was created successfully.
* }
* });
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* pubsub.detachSubscription('my-sub').then(data => {
* const apiResponse = data[0];
* });
*/
detachSubscription(
name: string,
optsOrCallback?: CallOptions | DetachSubscriptionCallback,
callback?: DetachSubscriptionCallback
): Promise<DetachSubscriptionResponse> | void {
if (typeof name !== 'string') {
throw new Error('A subscription name is required.');
}

const sub = this.subscription(name);
const reqOpts = {
subscription: sub.name,
};

const gaxOpts = typeof optsOrCallback === 'object' ? optsOrCallback : {};
callback = typeof optsOrCallback === 'function' ? optsOrCallback : callback;

this.request<google.pubsub.v1.IDetachSubscriptionRequest>(
{
client: 'PublisherClient',
method: 'detachSubscription',
reqOpts,
gaxOpts: gaxOpts as CallOptions,
},
callback!
);
}

/**
* Determine the appropriate endpoint to use for API requests, first trying
* the local `apiEndpoint` parameter. If the `apiEndpoint` parameter is null
Expand Down
48 changes: 48 additions & 0 deletions src/subscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import {google} from '../protos/protos';
import {IAM} from './iam';
import {FlowControlOptions} from './lease-manager';
import {
DetachedCallback,
DetachedResponse,
EmptyCallback,
EmptyResponse,
ExistsCallback,
Expand Down Expand Up @@ -83,6 +85,9 @@ export type GetSubscriptionMetadataResponse = MetadataResponse;
export type SetSubscriptionMetadataCallback = MetadataCallback;
export type SetSubscriptionMetadataResponse = MetadataResponse;

export type DetachSubscriptionCallback = EmptyCallback;
export type DetachSubscriptionResponse = EmptyResponse;

/**
* @typedef {object} ExpirationPolicy
* A policy that specifies the conditions for this subscription's expiration. A
Expand Down Expand Up @@ -528,6 +533,49 @@ export class Subscription extends EventEmitter {
);
}

detached(): Promise<DetachedResponse>;
detached(callback: DetachedCallback): void;
/**
* @typedef {array} SubscriptionDetachedResponse
* @property {boolean} 0 Whether the subscription is detached.
*/
/**
* @callback SubscriptionDetachedCallback
* @param {?Error} err Request error, if any.
* @param {boolean} exists Whether the subscription is detached.
*/
/**
* Check if a subscription is detached.
*
* @param {SubscriptionDetachedCallback} [callback] Callback function.
* @returns {Promise<SubscriptionDetachedResponse>}
*
* @example
* const {PubSub} = require('@google-cloud/pubsub');
* const pubsub = new PubSub();
*
* const topic = pubsub.topic('my-topic');
* const subscription = topic.subscription('my-subscription');
*
* subscription.detached((err, exists) => {});
*
* //-
* // If the callback is omitted, we'll return a Promise.
* //-
* subscription.detached().then((data) => {
* const detached = data[0];
* });
*/
detached(callback?: DetachedCallback): void | Promise<DetachedResponse> {
this.getMetadata((err, metadata) => {
if (err) {
callback!(err);
} else {
callback!(null, metadata!.detached);
}
});
}

exists(): Promise<ExistsResponse>;
exists(callback: ExistsCallback): void;
/**
Expand Down
18 changes: 18 additions & 0 deletions system-test/pubsub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ describe('pubsub', () => {
return generateName('subscription');
}

// This is a temporary situation - we'll eventually fall back to the
// regular generateSubName() call, but this has to be used for the
// pre-release allow list.
function generateSubForDetach() {
return `testdetachsubsxyz-${generateSubName()}`;
}

function generateTopicName() {
return generateName('topic');
}
Expand Down Expand Up @@ -352,10 +359,12 @@ describe('pubsub', () => {
const topic = pubsub.topic(TOPIC_NAME);

const SUB_NAMES = [generateSubName(), generateSubName()];
const SUB_DETACH_NAME = generateSubForDetach();

const SUBSCRIPTIONS = [
topic.subscription(SUB_NAMES[0], {ackDeadline: 30}),
topic.subscription(SUB_NAMES[1], {ackDeadline: 60}),
topic.subscription(SUB_DETACH_NAME, {ackDeadline: 30}),
];

before(async () => {
Expand Down Expand Up @@ -655,6 +664,15 @@ describe('pubsub', () => {
);
});

it('should detach subscriptions', async () => {
const subscription = topic.subscription(SUB_DETACH_NAME);
const [before] = await subscription.detached();
assert.strictEqual(before, false);
await pubsub.detachSubscription(SUB_DETACH_NAME);
const [after] = await subscription.detached();
assert.strictEqual(after, true);
});

// can be ran manually to test options/memory usage/etc.
// tslint:disable-next-line ban
it.skip('should handle a large volume of messages', async function () {
Expand Down
Loading

0 comments on commit c5af3a9

Please sign in to comment.