How to actively stop subscriptions efficiently

I am developing a Meteor app which just started getting traffic (thousands of concurrent sessions according to Google Analytics). Under such load the Meteor servers (hosted on AWS) return a high rate of 5xx responses.
The bulk part of the requests (99%) are read-only requests: subscriptions to collections which rarely change.

I’m looking for ways to dramatically reduce the Meteor servers load. Any ideas are welcome!

I’ve already reduced the subscriptions to the minimal amount of data necessary and followed advice given by Meteor-scaling blogs (such as mongodb indexing by relevant keys, using replica sets, users w/oplog tailing permissions). Nevertheless, the servers are still 5xxing and I’m guessing it’s because of the large amount of subscriptions.

As such, I’m willing to have the clients close most subscriptions after they receive data. Basically - converting the comm to (REST/AJAX)-like requests, only over DDP. Later on I plan to gradually and selectively increase the amount of open subscriptions.

I’ve searched how to properly stop a subscription. There seem to be a few methods, however they are bit ambiguous TMHO. I didn’t gain confidence that a specific solution is better.

Option 1

According to the Meteor docs, on the client it’s possible to:

let handler = Meteor.subscribe('items');
...
// subscription no longer necessary
handler.stop();

This option seems short, but after getting laggy client behavior I’ve discovered that when handler.stop() is called, the server sends multuple removed ddp messages (one for each document which the client subscribed to). In subscriptions where lots of documents are involved, this wastes time and bandwidth. I’ve noticed this also delayed new pending subscriptions which need bandwidth on the ddp channel.

I’d prefer an efficient way, as in dropping the whole subscription via one ddp message. Which leads me to:

Option 2

Actively end the subscription on the server in response to a new empty-subscription client request, as in:

Meteor.publish("items", function(criteria) {
  if (criteria) {
    return Messages.find({
      someField: criteria
    });
  } else {
    return this.stop();
  }
});

The problem with this approach is that AFAIK it’s not documented. I’ve bumped into it in a stackoverflow post so maybe it is wildly used?

Does anyone have experience with efficiently shutting down subscriptions on high-traffic meteor environments?
What’s the best method in your opinion?
Thanks!

7 Likes

Generally speaking, subscriptions shouldn’t contain so much data that stopping them causes problems. Of course, if they do, then you have to be careful when they are stopped.

This doesn’t do what you think it does.

When you call subscribe with no “criteria” it is a new subscription. this.stop() in that context is only stopping the new subscription, not the original. Also, you only ever want to return cursors in publications. In this case this.stop() returns void so you are safe.

A better way

Courtesy of MDG

Fortunately, MDG thought of this long ago. As long as you are using meteor 1.0.4 or later (you are using something less than a year old right?) then you have access to template level autoruns and subscriptions. React meteor data has this as well. These autoruns automatically end when your component is removed. A subscription in an autorun is invalidated when the autorun re-runs or stops. If no new subscription happens in some other autorun to match the invalidated one, then it will be stopped after flush. That last part is important.

Tracker.afterFlush() queues up functions to be run when the current round of reactive updates are done. This includes updates triggered by stopping autoruns, or creating new ones. Such events happen automatically as the view evolves, if you use template / component level autoruns. The beauty of this is that new data is requested before old subscriptions are stopped. That means that if a user goes to a new are of your app, and new subscriptions are required, they will trigger immediately, and get priority on the DDP connection. Old subscriptions will be cleaned up afterwards.

To use:

Blaze

Template.myTemplate.onCreated(function created() {
    this.autorun(() => {
        Meteor.subscribe('myImportantNewSubscription', withParameters);
    });
});

No need to do anything else. This will automatically be cleaned up when the template is removed from the view, but will do so after other templates trigger their autoruns.

React

React.createClass({
    mixins: [ReactMeteorData],
    getMeteorData() {
        Meteor.subscribe('myImportantNewSubscription', withParameters);
    }
});

Tracker

// for the purposes of actually getting a direct cleanup handle.
let handle = Tracker.autorun(() => {
    Meteor.subscribe('myImportantNewSubscription', withParameters);
});
handle.stop();

Reference

from https://github.com/meteor/meteor/blob/devel/packages/ddp-client/livedata_connection.js line 637

if (Tracker.active) {
    // We're in a reactive computation, so we'd like to unsubscribe when the
    // computation is invalidated... but not if the rerun just re-subscribes
    // to the same subscription!  When a rerun happens, we use onInvalidate
    // as a change to mark the subscription "inactive" so that it can
    // be reused from the rerun.  If it isn't reused, it's killed from
    // an afterFlush.
    Tracker.onInvalidate(function (c) {
        if (_.has(self._subscriptions, id))
            self._subscriptions[id].inactive = true;

        Tracker.afterFlush(function () {
            if (_.has(self._subscriptions, id) &&
                self._subscriptions[id].inactive)
                handle.stop();
        });
    });
}

You don’t really need an autorun function in templates. This will suffice AFAIK :sunny:

Template.foo.onCreated(function(){
    this.subscribe('bar')
})
2 Likes

Yep, using this.subscribe('bar'); in created handler is enough to create a template-bound subscription.

Also, are you using Kadira? It will tell you which parts of your app use most resources - which methods and publications take most CPU time. It may guide you in the right direction.

That doesn’t accomplish the goal of delaying the cleanup until other subscriptions have happened. To do that, it does have to be part of an autorun, as subscriptions hook into tracker in a special way in order to ensure that they get modified cleanly when tracker re-runs. That behavior also allows for delayed cleanup as it won’t actually unsub until all autoruns (and new dom elements) are done.

this.subscribe will stop it the moment the view is destroyed, which does happen before creating the new view. Thus an unsub will get in the way of new publications, especially if there are a lot of documents, as is the problem OP was dealing with.

2 Likes

Check if this helps you https://github.com/andrejsm/meteor-static-subs

While a useful package, that doesn’t solve the unsub case. Stopping a so-called static subscription will still cause: [quote=“tivoni, post:1, topic:19416”]
when handler.stop() is called, the server sends multuple removed ddp messages (one for each document which the client subscribed to). In subscriptions where lots of documents are involved, this wastes time and bandwidth. I’ve noticed this also delayed new pending subscriptions which need bandwidth on the ddp channel.
[/quote]

Edit: Went back and looked again at that package. It’s slightly different. There is no way to unsubscribe. This could be a solution if the OP wants to either a) keep their data locally or b) clean out local mongo themselves.

1 Like

Yes. Package is intended to just deliver data from server to client. It’s up to you how to flush data if that is required.
I use this for public data which I never flush.

Right, it works well for config type data, or MOTD type data which just doesn’t update, thus saving an observer server side. It doesn’t handle the stop aspect of the OP which is what this entire thread is about.

1 Like

I’m responding to this

1 Like

I’ll admit by the time I was done writing my first reply, I had forgotten that piece of the OP.

For that, your package might be the answer. It doesn’t answer the title/bulk of the OP but perhaps does answer the underlying problem.

1 Like

If the bulk of your requests are read-only for data that rarely changes, have you considered using Methods with local collections (if needed) instead? See the Loading data with Methods section of the Guide.

Exactly - that would be essentially what you’re doing with Methods.

2 Likes

awesome - this is exactly why I added “AFAIK” :sunny: thanks for the insight!

1 Like

Thanks all, you’ve provided great leads. Much appreciated!

Have you found the solution to the problem? If so, please do tell.

Somewhat.

We’ve realized we will not be able to support hundreds of thousands of reactive sessions on a few instances. We’d need to scale horizontally massively, which does not really justify the cost. As such we’re switching the (read-only) traffic to non-reactive connections i.e. rest APIs, probably CDN’ed. We’ll still use very selectively reactive sessions (sessions with a DDP websocket). But only for the users who really need those, i.e. the logged-in users which make changes and expect a reactive UX.
I hope that my “exploring and lessons-made” will be useful to those facing a similar dillema

2 Likes

THANK YOU!

Option two of your original post worked great. :smiley: