How to reuse Cron jobs with Collection on multiple sessions


#1

I have a collection which is intended to have a key value pair of a vehicle number and its lat lon value. I have a cron job which gets the lat lon information every 2 sec from an API for that vehicle number and removes and inserts the lat lon value for that collection with new lat lon value every time an API call is made(pls assume the lat lon value is different every time aPI call is made).

    Latloncoll.remove({vanNum:4567});
    Latloncoll.insert({
        vanNum:4567,
        latlonval: latlonValFromAPI
    });

In a scenario where multiple clients will be going through the same process of adding the cron job, when even a button is clicked from the client, i.e below method is called.

‘setCronLatestLatLonByVanNumber’:function(vanNum){
SyncedCron.add({
name: ‘Query the lat lon value from api’,
schedule: function(parser) {
return parser.text(‘every 2 sec’); /later.parse.recur().every(15).minute().startingOn(10);/
},
job: function() {
return Meteor.call(‘getVanLocByVanNumber’, vanNum);
}
});
}
Will there be multiple ever running cron jobs initiated by multiple users from the client call? i.e will there be duplicate collections created for each user on the mongo DB? or will it be a common collection for all the users?

My requirement is If a user initiates a cron job to get the location of the van number and stores in a collection, an other user trying to get the latest lat lon for the same van number should just be able to subscribe the latest lat lon from the cron job created by the previous user so that i can avoid multiple cron job creation and duplicate collections created for the same data.

Thanks


#2

I am wondering why I dont have a response to this question. Pls let me know if I am asking a stupid question or if this question is not clear at all?

Thanks


#3

Correct me if I’m wrong but can’t you check if a cron-job has already been queued up before you add another SyncedCron job?


#4

I dont know if that is the right solution. Since my question is about multiple sessions doing the same thing, will a cron job addition be specific to a session or is it common for all the sessions i.e will other clients be able to subscribe to the data published using cron job by a client?


#5

I will try this and will let you know the test results. Thanks


#6

I am unable to find any documentation around how to check if a synchedCron job already exists. Can someone pls help?


#7

From docs: “Call SyncedCron.nextScheduledAtDate(jobName) to find the date that the job
referenced by jobName will run next.”

So you can use that to check if a job exists and when it’s scheduled to run next.


#8

OK, so I’m a little confused by this. Is the client the vehicle driver or is the client the observer of vehicles? Does the vehicle client click a button to start/stop position sending?

Is it sufficient to have one cron job running independently and all the time (e.g. on a Meteor server timer)? That cron job updates a Mongo collection with the co-ordinates of every vehicle currently “active”. You will also need to give some thought to managing issues with the asynchronous nature of collecting vehicle GPS data remotely - you might be asking every 2 seconds, but there’s no guarantee that you will get a response back in that time (or ever). In fact, depending on cell proximity and local routing, you might get replies back out of sequence with requests.

As with any long-running process you will need to ensure that server resource usage is managed (no memory leaks), and that it will be restarted automatically if it stops (e.g. “respawn” in inittab on *nix systems).


#9

hi Rob,
the scenario is like this.

Customer A searches for a van number 5678 on the UI for him see the Van moving real time with out page refresh. The location of the lat lon of the van is available to meteor only by making an external API call from the meteor website. To achieve the real time view of changing van position on the map, I have created a cron job to keep polling for the van location based on the van number. (note I create the name of cron job with the van number attached to the name as the cron job is specific to the API call for that van number) . Below is the code

‘setCronLatestLatLonByVanNumber’:function(vanNum, token){
SyncedCron.add({
name: ‘GetLatestVanLocationByVanNumber’+vanNum,
schedule: function(parser) {
return parser.text(‘every 2 sec’);
},
job: function() {

            return Meteor.call('getVanLocByVanNumberFromAPI', vanNum, token);
        }
    });
}

What I do after the API call is insert the latlon value for the van number as a collection and delete the older lat lon value if the van has already got a lat lon value
If Customer B searches for the location on the same van number 5678, I would ideally want to reuse the same cron job for the van initiated by customer A as I dont kill the cron job if the customer A is not in use. Some times I think I need to kill the cron job initiated by customer A if customer A has logged out of the session and recreate the sych job for the same van number if customer B tries to find the moving van location .

I dont know if my thinking is correct and if you are able to understand my thinking. Pls let me know if i am doing the right thing.


#10

@vrangan: thanks for the PMs discussing this.

You have already identified some areas of complexity:

  • Giving users the ability (even if indirectly) to initiate cron jobs on the server
  • How to cancel those jobs when the user goes away
  • How to re-use data if the same van is being tracked.

The following code addresses all those concerns and does not use cron.

Assumptions

Base Collections

  • A collection for packages (tracking number, status, van number, customer id, etc.)
  • Other collections may be required (vans, customers, etc.)

Key Requirements

  • Tracking is initiated by the customer.
  • If no customers are tracking packages, no van location data is retrieved.
  • Tracking will be updated every 2 seconds (but see also the code annotation, below).
  • When vans are being tracked by multiple customers, GPS transponder requests will be throttled.

Suggested Implementation

I’ve made no attempt to:

  • address how the tracking number is obtained from the user
  • apply security checks to the method (is this user allowed to track this package?)
  • show any routing
  • show any sensible error handling
  • show how to present a moving marker on a map
  • use JSDoc annotation conventions (sorry - should have tried harder!)

Client Template (a .js file in the client/ folder)

Template.tracking.onCreated(function() {
  var self = this;
  self.current = new ReactiveDict();
  function getLocation() {
    // packageNumber needs to come from somewhere
    Meteor.call('track', packageNumber, function(error, result) {
      if (error) {
        // Do something if we get here because it broke in a bad way!
      } else {
        self.current.set(result);
        if (result.retry) {
          // We need to rerun in "retry" ms
          Meteor.setTimeout(getLocation, result.retry);
        }
      }
    });
  };
  getLocation(); //                         This runs once on creation and then every "retry" ms
});

Template.tracking.onRendered(function() {
  var self = this;
  self.autorun(function() { //              This will rerun whenever lat and/or long and/or status change.
    //                                      It's probably where your map marker rendering should live.
    var lat = self.current.get('lat');
    var long = self.current.get('long');
    var status = self.current.get('status');
    // do something with that stuff ...
  });
});

Template.tracking.helpers({ //              Not strictly necessary, but may be useful for Blaze templating
  unknown: function() {
    return Template.instance().current.get('status') === 'Unknown';
  },
  out: function() {
    return Template.instance().current.get('status') === 'Out';
  },
  depot: function() {
    return Template.instance().current.get('status') === 'Depot';
  },
  delivered: function() {
    return Template.instance().current.get('status') === 'Delivered';
  },
  lat: function() {
    return Template.instance().current.get('lat');
  },
  long: function() {
    return Template.instance().current.get('long');
  }
});

Server (a .js file in the server/ directory)

var Vans = {}; // We don't need on-disk persistence, so don't really need a collection, just a list of van objects.
// Remember, these will be shareable between and persist across the whole set of users as long as
// the app is running. Also much faster than (mini)mongo finds.

var getVanLocation = function(vanObj) { //  Polls the van's GPS transponder for its position
  // The van object from Vans[vanNumber]:
  //   vanNumber: The vanNumber
  //   time: The last time this van's position was collected
  //   lat: The latitude co-ordinate
  //   long: The longitude co-ordinate
  //   polling: internal flag
  try {
    // This may not be how you request the location!
    var result = HTTP.call(... vanObj.vanNumber ...);
    // and this may not be how you parse the result!
    // Note also, that this section (4 following vanObj lines) may be in a callback off the HTTP.call if that's preferred.

    vanObj.polling = false; //              Reset ready for next poll 
    vanObj.lat = result.content.lat; //     Position
    vanObj.long = result.content.long;
    vanObj.time = Date.now(); //            We need this to avoid spamming for van location polling

  } catch (error) {
    vanObj.polling = false; //              Allows another poll attempt
    throw new Meteor.Error('httpError', 'Could not poll the GPS transponder');
});

var whereIsMyVan = function(vanNumber) { // Debounces incoming requests
  var latency = 200; //                     Typical latency for polling a GPS transponder (ms)
  if (! Vans[vanNumber]) { //               Not seen a request for this van before, so get set up
    Vans[vanNumber] = {
      vanNumber: vanNumber, //              We'll need this for the getVanLocation function
      polling: true, //                     Used to indicate a van is being polled for its location
      lat: null, //                         Dummy co-ordinates
      long: null
    };
    getVanLocation(Vans[vanNumber]); //     Request van's location

  } else if (Vans[vanNumber].polling) { //  Van position request already in progress - return last position.
    return Vans[vanNumber]; //              Even if this is null, it will self-correct on the next request

  } else if (Date.now() - Vans[vanNumber].time > 2000 - latency) {

    // Last position older than 2-latency secs - get new one (this is what does the "debouncing").
    // Note that the latency adjustment is only needed when the number of polling clients is very small (1-3).
    // Even then, with one client and no latency adjustment, the worst case refresh on the client will be 4 seconds.

    Vans[vanNumber].polling = true; //      Indicate we're polling the van's GPS transponder
    getVanLocation(Vans[vanNumber]);
  }
  return Vans[vanNumber]; //                However we got here, return what we've got
});

Meteor.methods({ // If you require the user to be logged in, this is the place to ensure that's been done.
  track: function(trackingNumber) {
    var retry = null;
    var vanObj = {};
    try {
      var package = Packages.findOne({trackingNumber: trackingNumber});

      // Get the package info for the tracking number.
      // This code assumes a package document contains at least:
      //  trackingNumber: the customer's tracking number for this package
      //  status: 'Out', 'Depot', 'Delivered'
      //  vanNumber: the allocated van number for this package

      if (package) {
        if (package.status === 'Out') { //              Out for delivery
          retry = 2000; //                              The client should retry in 2 secs
          vanObj = whereIsMyVan(package.vanNumber); //  Update the results
        } else if (package.status === 'Depot') { //     Still in the depot
          retry = 60000; //                             The client should try again in 1 minute
        }

        // Return the location data. Note that lat and long may be null on the first call for this van.
        // The client may need to cope with that.

        return {
          status: package.status,
          retry: retry,
          lat: vanObj.lat,
          long: vanObj.long
        };
      } else {
        return {
          status: 'Unknown'
        };
      }
    } catch (error) {
      throw new Meteor.Error('Error', 'Something nasty happened');
    }
  }
});

I’ve run this code in a small app, where I’ve simulated the HTTP.call with a Meteor.setTimeout - i.e. asynchronous and having significant latency, with multiple connected clients and all works as expected.

I used the reactive-dict package for storing reactive template state.