Skip to content

Commit 308c6d1

Browse files
Salakarlaurenzlong
authored andcommitted
changed(firestore-counter): use pubsub trigger for controller function (#49)
1 parent cf7ed79 commit 308c6d1

File tree

16 files changed

+104
-66
lines changed

16 files changed

+104
-66
lines changed

firestore-counter/POSTINSTALL.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
### Post-installation configuration
22

3-
Before you can use this extension, you'll need to update your security rules, set up a scheduled function, and add some code to your JavaScript app.
3+
Before you can use this extension, you'll need to update your security rules, set up a Cloud Scheduler job, and add some code to your JavaScript app.
44

55
#### Update security rules
66

@@ -18,17 +18,23 @@ match /databases/{database}/documents/pages/{page} {
1818
}
1919
```
2020

21-
#### Set up a scheduled function
2221

23-
Review the [scheduled function documentation](https://firebase.google.com/docs/functions/schedule-functions) to set up a call to `${function:controller.url}` every minute. You may need to enable some APIs in your Firebase project to use scheduled functions.
22+
#### Set up a Cloud Scheduler job
2423

25-
As an example, to set up a scheduled function, you can run the following [`gcloud`](https://cloud.google.com/sdk/gcloud/) commands:
24+
**IMPORTANT:** Note the following about v0.1.1 of this extension:
25+
- **If you updated your extension from v0.1.0 to v0.1.1:** We recommend that you edit your Cloud Scheduler job to instead send a message to the extension's Pub/Sub topic, as described in this section. Although it's not recommended, if you leave your Cloud Scheduler job calling `${function:controller.url}`, your extension will continue to run as expected. For more information about the changes for v0.1.1, refer to the [changelog](https://github.com/firebase/extensions/blob/master/firestore-counter/CHANGELOG.md).
26+
- **If you installed this extension for the first time at v0.1.1:** Follow the instructions as described in this section.
27+
28+
Set up a [Cloud Scheduler job](https://cloud.google.com/scheduler/docs/quickstart) to regularly send a message to the extension's Pub/Sub topic (`${param:EXT_INSTANCE_ID}`). This Pub/Sub topic then automatically triggers the controllerCore function (`${function:controllerCore.name}`). This controllerCore function is created by the extension. It works by either aggregating shards itself or scheduling and monitoring workers to aggregate shards.
29+
30+
As an example, to set up the required Cloud Scheduler job, you can run the following `gcloud` commands:
2631

2732
```
28-
gcloud services enable cloudscheduler.googleapis.com
29-
gcloud scheduler jobs create http firestore-sharded-counter-controller --schedule="* * * * *" --uri=${function:controller.url} --project=${param:PROJECT_ID}
33+
gcloud --project=${param:PROJECT_ID} services enable cloudscheduler.googleapis.com
34+
gcloud --project=${param:PROJECT_ID} scheduler jobs create pubsub ${param:EXT_INSTANCE_ID} --schedule="* * * * *" --topic=${param:EXT_INSTANCE_ID} --message-body="{}"
3035
```
3136

37+
3238
#### Specify a document path and increment value in your app
3339

3440
1. Download and copy the [Counter SDK](https://github.com/firebase/extensions/blob/master/firestore-counter/clients/web/dist/sharded-counter.js) into your application project.
@@ -77,7 +83,7 @@ After you complete the post-installation configuration above, the process runs a
7783

7884
1. The client SDK writes to these subcollections to distribute the write load.
7985

80-
1. The scheduled function that you deployed sums the subcollections' values into the single `visits` field (or whichever field you configured in your master document).
86+
1. The controllerCore function sums the subcollections' values into the single `visits` field (or whichever field you configured in your master document).
8187

8288
1. After each summation, the extension deletes the subcollections, leaving only the count in the master document. This is the document field to which you should listen for the count.
8389

firestore-counter/PREINSTALL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Before installing this extension, make sure that you've [set up a Cloud Firestor
1818
After installing this extension, you'll need to:
1919

2020
- Update your [database security rules](https://firebase.google.com/docs/rules).
21-
- Set up a [scheduled function](https://firebase.google.com/docs/functions/schedule-functions) to regularly call the controller function, which is created by this extension and monitors the extension's workload.
21+
- Set up a [Cloud Scheduler job](https://cloud.google.com/scheduler/docs/quickstart) to regularly call the controllerCore function, which is created by this extension. It works by either aggregating shards itself or scheduling and monitoring workers to aggregate shards.
2222
- Install the provided [Counter SDK](https://github.com/firebase/extensions/blob/master/firestore-counter/clients/web/src/index.ts) in your app. You can then use this library in your code to specify your document path and increment values.
2323

2424
Detailed information for these post-installation tasks are provided after you install this extension.

firestore-counter/extension.yaml

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,32 @@ contributors:
3535

3636
roles:
3737
- role: datastore.user
38-
reason: Allows the extension to aggregate Cloud Firestore counter shards.
38+
reason:
39+
Allows the extension to aggregate Cloud Firestore counter shards.
40+
- role: pubsub.publisher
41+
reason:
42+
Allows the HTTPS controller function to publish a message to the extension's Pub/Sub topic,
43+
which triggers the controllerCore function.
3944

4045
resources:
41-
- name: controller
46+
- name: controllerCore
4247
type: firebaseextensions.v1beta.function
4348
description:
4449
Scheduled to run every minute.
4550
This function either aggregates shards itself, or it schedules and monitors workers to aggregate shards.
51+
properties:
52+
sourceDirectory: .
53+
location: ${LOCATION}
54+
maxInstances: 1
55+
eventTrigger:
56+
eventType: google.pubsub.topic.publish
57+
resource: projects/${PROJECT_ID}/topics/${EXT_INSTANCE_ID}
58+
59+
- name: controller
60+
type: firebaseextensions.v1beta.function
61+
description:
62+
Maintained for backwards compatibility.
63+
This function relays a message to the extension's Pub/Sub topic to trigger the controllerCore function.
4664
properties:
4765
sourceDirectory: .
4866
location: ${LOCATION}
@@ -67,7 +85,7 @@ resources:
6785
description:
6886
Monitors a range of shards and aggregates them, as needed.
6987
There may be 0 or more worker functions running at any point in time.
70-
The controller function is responsible for scheduling and monitoring these workers.
88+
The controllerCore function is responsible for scheduling and monitoring these workers.
7189
properties:
7290
sourceDirectory: .
7391
location: ${LOCATION}
@@ -108,4 +126,8 @@ params:
108126
What is the path to the document where the extension can keep its internal state?
109127
default: _firebase_ext_/sharded_counter
110128
example: _firebase_ext_/sharded_counter
129+
validationRegex: "^[^/]+/[^/]+(/[^/]+/[^/]+)*$"
130+
validationErrorMessage:
131+
Enter a document path, not a collection path. The path must have an even number of segments,
132+
for example, `my_collection/doc` or `my_collection/doc/subcollection/doc`, but not `my_collection`.
111133
required: true

firestore-counter/functions/lib/aggregator.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class NumericUpdate {
2424
/**
2525
* Merges numeric values from an arbitrary deep json into the NumericUpdate object.
2626
* - it ignores non-numeric leaves
27-
* - if there's a type mismatch ('number' vs 'object') current data will be overriden
27+
* - if there's a type mismatch ('number' vs 'object') current data will be overridden
2828
* @param from An object with numeric values to merge from.
2929
*/
3030
mergeFrom(from) {
@@ -33,7 +33,7 @@ class NumericUpdate {
3333
/**
3434
* Subtracts numeric values in an arbitrary deep json from the NumericUpdate object.
3535
* - it ignores non-numeric leaves
36-
* - if there's a type mismatch ('number' vs 'object') current data will be overriden
36+
* - if there's a type mismatch ('number' vs 'object') current data will be overridden
3737
* @param from An object with numeric values to merge from.
3838
*/
3939
subtractFrom(from) {

firestore-counter/functions/lib/common.js

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,6 @@
1414
* See the License for the specific language governing permissions and
1515
* limitations under the License.
1616
*/
17-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
18-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
19-
return new (P || (P = Promise))(function (resolve, reject) {
20-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
21-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
22-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
23-
step((generator = generator.apply(thisArg, _arguments || [])).next());
24-
});
25-
};
2617
Object.defineProperty(exports, "__esModule", { value: true });
2718
function isUpdatedFrequently(shard) {
2819
if (!shard.exists)
@@ -58,9 +49,3 @@ function queryRange(db, collectionId, start, end, limit) {
5849
return query;
5950
}
6051
exports.queryRange = queryRange;
61-
function delay(ms) {
62-
return __awaiter(this, void 0, void 0, function* () {
63-
return new Promise((resolve) => setTimeout(resolve, ms));
64-
});
65-
}
66-
exports.delay = delay;

firestore-counter/functions/lib/controller.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ class ShardedCounterController {
191191
yield t.get(this.controllerDocRef);
192192
}
193193
catch (err) {
194-
console.log("Failed to read controler doc " + this.controllerDocRef.path);
194+
console.log("Failed to read controller doc " + this.controllerDocRef.path);
195195
throw Error("Failed to read controller doc.");
196196
}
197197
// Read all workers' metadata and construct sharding info based on collected stats.

firestore-counter/functions/lib/index.js

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
2626
Object.defineProperty(exports, "__esModule", { value: true });
2727
const functions = require("firebase-functions");
2828
const admin = require("firebase-admin");
29+
const pubsub_1 = require("@google-cloud/pubsub");
2930
const worker_1 = require("./worker");
3031
const controller_1 = require("./controller");
3132
admin.initializeApp();
3233
const firestore = admin.firestore();
3334
firestore.settings({ timestampsInSnapshots: true });
35+
let pubsub;
3436
const SHARDS_COLLECTION_ID = "_counter_shards_";
3537
const WORKERS_COLLECTION_ID = "_counter_workers_";
3638
/**
37-
* Controller is scheduled every minute. It tries to aggregate shards if
39+
* The controllerCore is scheduled every minute. It tries to aggregate shards if
3840
* there's less than 200 of them. Otherwise it is scheduling and monitoring
3941
* workers to do the aggregation.
4042
*/
41-
exports.controller = functions.https.onRequest((req, res) => __awaiter(void 0, void 0, void 0, function* () {
43+
exports.controllerCore = functions.handler.pubsub.topic.onPublish(() => __awaiter(void 0, void 0, void 0, function* () {
4244
const metadocRef = firestore.doc(process.env.INTERNAL_STATE_PATH);
4345
const controller = new controller_1.ShardedCounterController(metadocRef, SHARDS_COLLECTION_ID);
4446
let status = yield controller.aggregateOnce({ start: "", end: "" }, 200);
@@ -47,14 +49,26 @@ exports.controller = functions.https.onRequest((req, res) => __awaiter(void 0, v
4749
status === controller_1.ControllerStatus.FAILURE) {
4850
yield controller.rescheduleWorkers();
4951
}
50-
res.status(200).send("OK");
52+
return null;
53+
}));
54+
/**
55+
* Backwards compatible HTTPS function
56+
*/
57+
exports.controller = functions.https.onRequest((req, res) => __awaiter(void 0, void 0, void 0, function* () {
58+
if (!pubsub) {
59+
pubsub = new pubsub_1.PubSub();
60+
}
61+
yield pubsub
62+
.topic(process.env.EXT_INSTANCE_ID)
63+
.publish(Buffer.from(JSON.stringify({})));
64+
res.status(200).send("Ok");
5165
}));
5266
/**
5367
* Worker is responsible for aggregation of a defined range of shards. It is controlled
5468
* by a worker metadata document. At the end of its run (that lasts for 45s) it writes
5569
* back stats that kicks off another run at the same time.
5670
*
57-
* Controller is monitoring these metadata documents to detect overload that requires
71+
* ControllerCore is monitoring these metadata documents to detect overload that requires
5872
* resharding and to detect failed workers that need poking.
5973
*/
6074
exports.worker = functions.firestore
@@ -69,7 +83,7 @@ exports.worker = functions.firestore
6983
/**
7084
* This is an additional function that is triggered for every shard write. It is
7185
* limited to one concurrent run at the time. This helps reduce latency for workloads
72-
* that are below the theshold for workers.
86+
* that are below the threshold for workers.
7387
*/
7488
exports.onWrite = functions.firestore
7589
.document("/{collection}/{**}/_counter_shards_/{shardId}")

firestore-counter/functions/lib/worker.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const WORKER_TIMEOUT_MS = 45000;
4444
*
4545
* Workers avoid double scheduling and overruns by including their metadata documents in every
4646
* aggregation transaction. If metadata changes underneath, transaction fails, worker detects that
47-
* and terminates immmediately.
47+
* and terminates immediately.
4848
*/
4949
class ShardedCounterWorker {
5050
constructor(metadoc, shardCollection, singleRun = false) {

firestore-counter/functions/src/aggregator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
*/
1616

1717
import { firestore } from "firebase-admin";
18-
import { FieldPath, FieldValue } from "@google-cloud/firestore";
18+
import { FieldValue } from "@google-cloud/firestore";
1919
import * as uuid from "uuid";
2020

2121
export class NumericUpdate {
@@ -24,7 +24,7 @@ export class NumericUpdate {
2424
/**
2525
* Merges numeric values from an arbitrary deep json into the NumericUpdate object.
2626
* - it ignores non-numeric leaves
27-
* - if there's a type mismatch ('number' vs 'object') current data will be overriden
27+
* - if there's a type mismatch ('number' vs 'object') current data will be overridden
2828
* @param from An object with numeric values to merge from.
2929
*/
3030
public mergeFrom(from: { [key: string]: any }) {
@@ -33,7 +33,7 @@ export class NumericUpdate {
3333
/**
3434
* Subtracts numeric values in an arbitrary deep json from the NumericUpdate object.
3535
* - it ignores non-numeric leaves
36-
* - if there's a type mismatch ('number' vs 'object') current data will be overriden
36+
* - if there's a type mismatch ('number' vs 'object') current data will be overridden
3737
* @param from An object with numeric values to merge from.
3838
*/
3939
public subtractFrom(from: { [key: string]: any }) {

firestore-counter/functions/src/common.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,6 @@
1515
*/
1616

1717
import { firestore } from "firebase-admin";
18-
import * as path from "path";
19-
import { FieldPath } from "@google-cloud/firestore";
20-
import { ShardedCounterController } from "./controller";
2118

2219
/**
2320
* Represents a document range that a single worker is responsible for.
@@ -81,7 +78,3 @@ export function queryRange(
8178
query = query.limit(limit);
8279
return query;
8380
}
84-
85-
export async function delay(ms: number): Promise<void> {
86-
return new Promise<void>((resolve) => setTimeout(resolve, ms));
87-
}

0 commit comments

Comments
 (0)