38

I have the following code in server/statusboard.js;

var require = __meteor_bootstrap__.require,
    request = require("request")   


function getServices(services) {
  services = [];
  request('http://some-server/vshell/index.php?type=services&mode=json', function (error, response, body) {
    var resJSON = JSON.parse(body);
     _.each(resJSON, function(data) {
       var host = data["host_name"];
       var service = data["service_description"];
       var hardState = data["last_hard_state"];
       var currState = data["current_state"];
       services+={host: host, service: service, hardState: hardState, currState: currState};
       Services.insert({host: host, service: service, hardState: hardState, currState: currState});
    });
  });
}

Meteor.startup(function () {
  var services = [];
  getServices(services);
  console.log(services);
});

Basically, it's pulling some data from a JSON feed and trying to push it into a collection.

When I start up Meteor I get the following exception;

app/packages/livedata/livedata_server.js:781
      throw exception;
            ^
Error: Meteor code must always run within a Fiber
    at [object Object].withValue (app/packages/meteor/dynamics_nodejs.js:22:15)
    at [object Object].apply (app/packages/livedata/livedata_server.js:767:45)
    at [object Object].insert (app/packages/mongo-livedata/collection.js:199:21)
    at app/server/statusboard.js:15:16
    at Array.forEach (native)
    at Function.<anonymous> (app/packages/underscore/underscore.js:76:11)
    at Request._callback (app/server/statusboard.js:9:7)
    at Request.callback (/usr/local/meteor/lib/node_modules/request/main.js:108:22)
    at Request.<anonymous> (/usr/local/meteor/lib/node_modules/request/main.js:468:18)
    at Request.emit (events.js:67:17)
Exited with code: 1

I'm not too sure what that error means. Does anyone have any ideas, or can suggest a different approach?

Artjom B.
  • 61,146
  • 24
  • 125
  • 222
Andrew Beresford
  • 566
  • 1
  • 4
  • 11
  • I should make it clear that "Services" has been defined elsewhere in a seperate file (common to both the client and server). – Andrew Beresford Apr 17 '12 at 14:18
  • This line is the issue: `Services.insert({host: host, service: service, hardState: hardState, currState: currState});` I think its because its in a callback, no way to test at the moment for you. – jonathanKingston Apr 17 '12 at 14:31
  • 2
    Meteor now includes an HTTP request library that makes your case a lot easier: http://docs.meteor.com/#meteor_http – debergalis May 06 '12 at 16:13

3 Answers3

48

Just wrapping your function in a Fiber might not be enough and can lead to unexpected behavior.

The reason is, along with Fiber, Meteor requires a set of variables attached to a fiber. Meteor uses data attached to a fiber as a dynamic scope and the easiest way to use it with 3rd party api is to use Meteor.bindEnvironment.

T.post('someurl', Meteor.bindEnvironment(function (err, res) {
  // do stuff
  // can access Meteor.userId
  // still have MongoDB write fence
}, function () { console.log('Failed to bind environment'); }));

Watch these videos on evented mind if you want to know more: https://www.eventedmind.com/posts/meteor-dynamic-scoping-with-environment-variables https://www.eventedmind.com/posts/meteor-what-is-meteor-bindenvironment

imslavko
  • 6,596
  • 2
  • 34
  • 43
  • 1
    This did the trick for me. The accepted answer didn't work, because `Fiber` does not exist! – Jeanluca Scaljeri May 26 '14 at 18:28
  • 1
    Fibers only exist on the server-side, did you put it in common or clientside code? – Thomas Sep 28 '14 at 05:01
  • what about nested callbacks? for example 1-(readFile) 2-(upload file to s3) 3-(then save returned url to db). Do we need to use bindEnvironment on each callback or only the first one? – Murat Ozgul May 25 '16 at 01:35
15

As mentioned above it is because your executing code within a callback.

Any code you're running on the server-side needs to be contained within a Fiber.

Try changing your getServices function to look like this:

function getServices(services) {
  Fiber(function() { 
    services = [];
    request('http://some-server/vshell/index.php?type=services&mode=json', function (error, response, body) {
      var resJSON = JSON.parse(body);
       _.each(resJSON, function(data) {
         var host = data["host_name"];
         var service = data["service_description"];
         var hardState = data["last_hard_state"];
         var currState = data["current_state"];
         services+={host: host, service: service, hardState: hardState, currState: currState};
         Services.insert({host: host, service: service, hardState: hardState, currState: currState});
      });
    });
  }).run();  
}

I just ran into a similar problem and this worked for me. What I have to say though is that I am very new to this and I do not know if this is how this should be done.

You probably could get away with only wrapping your insert statement in the Fiber, but I am not positive.

Zeman4323
  • 190
  • 1
  • 3
7

Based on my tests you have to wrap the insert in code I tested that is similar to the above example.

For example, I did this and it still failed with Fibers error.

function insertPost(args) {
  if(args) {
Fiber(function() { 
    post_text = args.text.slice(0,140);
    T.post('statuses/update', { status: post_text }, 
        function(err, reply) {          
            if(reply){
                // TODO remove console output
                console.log('reply: ' + JSON.stringify(reply,0,4));
                console.log('incoming twitter string: ' + reply.id_str);
                // TODO insert record
                var ts = Date.now();
                id = Posts.insert({
                    post: post_text, 
                    twitter_id_str: reply.id_str,
                    created: ts
                });
            }else {
                console.log('error: ' + JSON.stringify(err,0,4));
                // TODO maybe store locally even though it failed on twitter
                // and run service in background to push them later?
            }
        }
    );
}).run();
  }
}

I did this and it ran fine with no errors.

function insertPost(args) {
  if(args) { 
post_text = args.text.slice(0,140);
T.post('statuses/update', { status: post_text }, 
    function(err, reply) {          
        if(reply){
            // TODO remove console output
            console.log('reply: ' + JSON.stringify(reply,0,4));
            console.log('incoming twitter string: ' + reply.id_str);
            // TODO insert record
            var ts = Date.now();
            Fiber(function() {
                id = Posts.insert({
                    post: post_text, 
                    twitter_id_str: reply.id_str,
                    created: ts
                });
            }).run();
        }else {
            console.log('error: ' + JSON.stringify(err,0,4));
            // TODO maybe store locally even though it failed on twitter
            // and run service in background to push them later?
        }
    }
);
  }
}

I thought this might help others encountering this issue. I have not yet tested calling the asynchy type of external service after internal code and wrapping that in a Fiber. That might be worth testing as well. In my case I needed to know the remote action happened before I do my local action.

Hope this contributes to this question thread.

Steeve Cannon
  • 3,682
  • 3
  • 36
  • 49
  • [02:33:57] that's a bad answer, i should comment on that. [02:34:20] that does create a Fiber, but it doesn't set up the useful meteor context inside it, and it doesn't actually block the outer method on the inner thread. – Tamara Wijsman Jun 07 '12 at 00:34
  • 2
    @TomWijsman which is accurate then? Fiber around the entire block of code in the method? – Steeve Cannon Jun 07 '12 at 08:18
  • 1
    [01:40:21] 1: unhook the write fence. see the implementation of Meteor.setTimeout in packages/meteor/timers.js for an example of this. [01:41:19] 2: wrap whatever callback you've defined in a new fiber, so that the method doesn't return until your callback runs. that's how synchronous meteor APIs are implemented, like in packages/mongo-livedata/mongo_driver.js [01:42:02] the right answer is actually 3: yell at us for not implementing a decent synchronous API for whatever you're trying to do :) – Tamara Wijsman Jun 07 '12 at 13:37
  • *Not deberg's words:* It should be noted that #2 required one to use FUTURES from node-fibers, so it's a bit different from your approach. In any case, while this might work in your specific usage it might not be the right way and don't work reliable for everyone. We should really get #3 working... – Tamara Wijsman Jun 07 '12 at 13:39
  • 1
    @TomWijsman I see. BTW, the other versions of using Fibers on this thread are similar to what I did and work. They too need #3? – Steeve Cannon Jun 07 '12 at 17:03
  • This worked great for me! Thanks for sharing. Also, to me this seems like the simplest approach out of the bunch. I don't care about the Meteor context in my case.... besides, what are you going to get out of the context, the userId?? I don't need that. – Aaron Apr 11 '15 at 02:51
  • I just noticed, this was posted in '12? It still works -- that's great! – Aaron Apr 11 '15 at 02:52
  • what if one wanted to return a value , how can we return a value within a Fiber – Vinay Prabhakaran Dec 01 '16 at 12:12