2015-01-08 43 views
1

短篇:如果该方法在超过120秒执行流星方法调用循环

流星方法循环。

这是我的测试

中有如下的服务器端称为“构建”流星方法:

'build': function(buildRequest) { 

    log.info('method build get called: buildRequest=%s', JSON.stringify(buildRequest)); 

    shell.exec('echo "simulate long Android build task."'); 
    shell.exec('sleep ' + buildRequest.sec); 

    var result = {'successful': true, 'output': "OK", 'fileName': 'test.apk', 'genFile': '/tmp/test.apk'} 

    log.info("method return result=%s", JSON.stringify(result)); 
    return result; 
} 

我设定的路线来调用这个方法如下:

this.route('buildApp', { 
    where: 'server' 
    , action: function() { 
    var buildRequest = this.request.query; 
    log.info('buildApp: http parameters: ', buildRequest); 

    var result = methods.build(buildRequest); 
    var response = this.response; 

    if (result.successful) { 
     methods.download(response, result.genFile, result.fileName); 
    } 
    else { 
     response.writeHead(500, {}); 
     response.end("server has error: " + result.output); 
    } 
    } 
}) 

然后,我打电话url

​​

然后,构建方法循环

=> App running at: http://localhost:3000/ 
I20150109-14:55:45.285(9)? info: buildApp: http parameters: app=test, server=dev, db=DB, appId=test, sec=120 
I20150109-14:55:45.358(9)? info: method build get called: buildRequest={"app":"test","server":"dev","db":"DB","appId":"test","sec":"120"} 
I20150109-14:55:45.358(9)? simulate long Android build task. 
I20150109-14:57:45.359(9)? info: method return result={"successful":true,"output":"OK","fileName":"test.apk","genFile":"/tmp/test.apk"} 
I20150109-14:57:45.387(9)? info: buildApp: http parameters: app=test, server=dev, db=DB, appId=test, sec=120 
I20150109-14:57:45.387(9)? info: method build get called: buildRequest={"app":"test","server":"dev","db":"DB","appId":"test","sec":"120"} 
I20150109-14:57:45.446(9)? simulate long Android build task. 

我认为它是与此代码:

https://github.com/meteor/meteor/blob/096df9d62dc9c3d560d31b546365f6bdab5a87dc/packages/webapp/webapp_server.js#L18

长的故事:

我做了一个简单的Android应用程序构建屏Meteor。 一切正常,但是如果我提交表单来构建应用程序,它会一遍又一遍地构建。即使我停止服务器并重新启动,只要服务器重新启动,它就会再次调用。

如果表格填写并提交,我打电话Meteor'建立'的方法。 该方法将克隆git存储库并通过调用下面的shell脚本来构建应用程序。

var exec = shell.exec('./genApp.sh ' + buildRequest.appId + " " + buildRequest.server + " " + buildRequest.db); 
//var exec = shell.exec('echo "simple task will not loop"'); 

如果我把./genApp.sh(这将需要几分钟的时间。),然后是流星“构建”的方法循环本身。但如果我做了简单的任务,它不会循环但执行一次。

我在构建Meteor方法的开始处添加了下面的代码以停止它进行调试。但我不知道是什么原因造成的。

 if (a == 1) { 
     a = 0; 
     throw new Error("Why call again?!"); 
     } 
     ++ a; 

服务器日志:

I20150108-19:48:08.220(9)? info: success 
I20150108-19:48:08.221(9)? info: return result=[object Object] 
I20150108-19:48:09.034(9)? Exception while invoking method 'build' Error: Why call again?! 
I20150108-19:48:09.035(9)?  at [object Object].methods.build (app/javis.js:92:25) 
I20150108-19:48:09.035(9)?  at maybeAuditArgumentChecks (packages/ddp/livedata_server.js:1599:1) 
I20150108-19:48:09.035(9)?  at packages/ddp/livedata_server.js:648:1 
I20150108-19:48:09.035(9)?  at [object Object]._.extend.withValue (packages/meteor/dynamics_nodejs.js:56:1) 
I20150108-19:48:09.035(9)?  at packages/ddp/livedata_server.js:647:1 
I20150108-19:48:09.035(9)?  at [object Object]._.extend.withValue (packages/meteor/dynamics_nodejs.js:56:1) 
I20150108-19:48:09.036(9)?  at [object Object]._.extend.protocol_handlers.method (packages/ddp/livedata_server.js:646:1) 
I20150108-19:48:09.036(9)?  at packages/ddp/livedata_server.js:546:1 

主要源代码

var APP_01 = 'app01-andy'; 
var APP_02 = 'app02-andy'; 

if (Meteor.isClient) { 

    Router.configure({ 
    layoutTemplate: 'layout' 
    }) 
    Router.map(function() { 
    this.route('/', 'home'); 
    }); 

    buildRequest = { 
    appId: 0 
    , server: 'dev' 
    , db: 'DEFAULT' 
    , app: APP_01 
    }; 

    Session.set('successful', false); 
    Session.set('output', ''); 
    Session.set('downloadLink', null); 

    Template.home.helpers({ 
    successful: function() { 
     return Session.get('successful'); 
    } 
    , output: function() { 
     return Session.get('output'); 
    } 
    , downloadLink: function() { 
     var successful = Session.get('successful'); 
     var downloadLink = Session.get('downloadLink'); 
     console.log(downloadLink); 
     if (successful) { 
     $('#downloadLink').show(); 
     } else { 
     $('#downloadLink').hide(); 
     } 
     return downloadLink; 
    } 
    }); 

    Template.home.events({ 
    'submit .app-build-form': function() { 
     event.preventDefault(); 

     buildRequest.appId = event.target.appId.value; 
     buildRequest.server = event.target.server.value; 
     buildRequest.db = event.target.db.value; 
     buildRequest.app = event.target.app.value; 

     $("#submit").prop('disabled', true); 

     Meteor.call('build', buildRequest, function(error, result) { 
     console.log(JSON.stringify(result)); 
     Session.set('successful', result.successful); 
     Session.set('output', result.output); 
     Session.set('downloadLink', '/downloadApp?fullPathFile='+result.genFile+'&fileName='+result.fileName); 
     $("#submit").prop('disabled', false); 
     console.log("meteor call end"); 
     }); 

     console.log("submit finished"); 
     // prevent default form submit. 
     return false; 
    }, 
    'click #sel_app': function() { 
     console.log(event.target.value); 
     var app = event.target.value; 
     var selDb = $("#sel_db"); 
     if (app === APP_02) { 
     selDb.val('APP_02_DB'); selDb.prop('disabled', true); 
     } 
     else { 
     selDb.prop('disabled', false); 
     } 
    } 
    }); 
} 

if (Meteor.isServer) { 

    var shell = Meteor.npmRequire('shelljs'); 
    var log = Meteor.npmRequire('winston'); 

    var a = 0; 

    var methods = { 
    'build': function(buildRequest) { 
     if (a == 1) { 
     a = 0; 
     throw new Error("Why call again?!"); 
     } 
     ++ a; 
     log.info(JSON.stringify(buildRequest)); 
     var dir = shell.pwd(); 
     log.info("work dir: %s", dir); 

     shell.cd('/project/build/'); 

     var branch = null; 
     var app = buildRequest.app; 
     if (app === APP_01) { 
     branch = '2.0'; 
     } 
     else if (app === APP_02) { 
     branch = '1.0'; 
     } 
     else { 
     branch = 'master'; 
     } 
     shell.exec('rm -rf ' + buildRequest.app); 
     shell.exec('git clone -b '+branch+' ssh://[email protected]/'+buildRequest.app+'.git'); 
     shell.cd(buildRequest.app + "/app"); 

     var exec = shell.exec('./genApp.sh ' + buildRequest.appId + " " + buildRequest.server + " " + buildRequest.db); 
     //var exec = shell.exec('echo "simple task will not loop"'); 

     var code = exec.code; 
     var output = exec.output; 

     log.info(code); 
     log.info(output); 

     var fileName = null; 
     var matches = output.match(/The package copied to (.+apk)/); 
     var genFile = null; 
     if (matches != null && matches.length > 1) { 
     genFile = matches[1]; 
     log.info(genFile); 
     // TODO : do not write file in public, http://stackoverflow.com/questions/13201723/generating-and-serving-static-files-with-meteor 
     /* 
     shell.mkdir(process.env.PWD + '/tmp'); 
     shell.cp('-f', genFile, process.env.PWD + '/tmp'); 
     */ 
     fileName = genFile.substring(genFile.lastIndexOf('/') + 1); 
     } 
     matches = output.match(/BUILD SUCCESSFUL/); 
     var successful = false; 
     if (matches != null && matches.length > 0) { 
     log.info("success"); 
     successful = true; 
     } 

     var result = {'successful': successful, 'output': output, 'fileName': fileName, 'genFile': genFile}; 

     log.info("return result="+result); 
     return result; 
    } 
    , 'download' : function(response, fullPathFile, fileName) { 

     var stat = fs.statSync(fullPathFile); 
     response.writeHead(200, { 
     'Content-Type': 'application/vnd.android.package-archive' 
     , 'Content-Length': stat.size 
     , 'Content-Disposition': 'attachment; filename=' + fileName 
     }); 
     fs.createReadStream(result.genFile).pipe(response); 
    } 
    }; 

    Meteor.methods(methods); 

    fs = Npm.require('fs'); 

    Router.map(function() { 
    this.route('buildApp', { 
     where: 'server' 
     , action: function() { 
     var buildRequest = this.request.query; 
     log.info(this.request.query); 
     log.info(buildRequest); 
     var result = methods.build(buildRequest); 
     var response = this.response; 

     if (result.successful) { 
      methods.download(response, result.genFile, result.fileName); 
     } 
     else { 
      response.writeHead(500, {}); 
      response.end("server has error: " + result.output); 
     } 
     } 
    }), 
    this.route('downloadApp', { 
     where: 'server' 
     , action: function() { 
     var params = this.request.query; 
     var fullPathFile = params.fullPathFile; 
     var fileName = params.fileName; 
     methods.download(this.response, fullPathFile, fileName); 
     } 
    }) 
    }); 

    Meteor.startup(function() { 
    // code to run on server at startup 
    }); 
} 

是什么原因导致的循环?任何帮助将不胜感激。

即使我打电话http://localhost:3000/buildApp?app=xxx&server=live&db=DB&appId=12423,构建循环。

好的,如果我改变./genApp.sh为简单的一个来缩小条件。

#!/bin/bash 

echo "test" 
echo "The package copied to /service/release/20150108/existing-file.apk" 
echo "BUILD SUCCESSFUL" 

sleep 180 

它在睡眠180秒后再次调用。又是什么使这个电话?因为我直接调用url。我认为没有客户端代码重审。

+0

嗯..所以你解压缩文件到您的项目目录?更改文件会导致“热代码推送”,这会导致当前“未完成”方法在流星重新加载后重新运行(或者您的浏览器因未收到响应而再次尝试)。一个快速测试是把它们放在名称末尾带有'〜'的文件夹下。 –

+0

我将生成的文件复制到“公共”目录中,但我知道这不是好的方法,所以我不拷贝它并在服务器被调用/ downloadApp?fileName = xxx时读写它,所以我假设没有热代码 - 推,现在但仍然重新发生的事情。 – Chk0nDanger

+0

你在读/写什么目录? –

回答

0

嗯......这可能是因为当shell.exec同步运行时,它“饿死事件循环”,导致一些问题。

解决方法是使用Future这将允许流星继续在另一个Fiber中运行,同时等待shell.exec完成。

var Future = Npm.require('fibers/future'); 

Meteor.methods({ 
'build': function(buildRequest) { 
    var fut = new Future(), result; 
    log.info('method build get called: buildRequest=%s', JSON.stringify(buildRequest)); 

    shell.exec('echo "simulate long Android build task."'); 
    shell.exec('sleep ' + buildRequest.sec, function(code, output){ 
    fut.return([code, output]); 
    }); 
    result = fut.wait(); 
    log.info(result[0], result[1]) 

    var result = {'successful': true, 'output': "OK", 'fileName': 'test.apk', 'genFile': '/tmp/test.apk'} 

    log.info("method return result=%s", JSON.stringify(result)); 
    return result; 
} 
}); 
+0

感谢您的回答,但它是同样的事情,它循环调用,永远不会写入文件。我认为这是因为如果Meteor方法运行超过120秒,它会通过websocket再次调用。所以如果一些任务花费的时间超过120秒,应该以不同的方式实施,我是一个没有多少探索流星世界的地球人。所以不知道如何以流星的方式来解决这个问题。不过,我认为发布/订阅模式可以是一种方式。或者有没有一种方法异步调用服务器方法(流星方法)? – Chk0nDanger

+1

嗯..我稍后会尝试一些测试。有趣的是,我有一些调用Web服务的方法,需要很长时间才能完成;所以如果有这样的限制,会很奇怪。此外,这里给出的答案仍然很重要 - 不要使用长时间运行的同步功能,除非它们与光纤/期货兼容。 –

+0

@ Chk0nDanger您指向的流星源只是针对'HTTP'请求 - 不适用于对'Meteor.methods'的调用;它可能适用于方法调用。但是我会发布另一个答案。 –

0

傻瓜证明的方式来完成这项工作:

  1. 创建一个集合来存储“建设的要求”
  2. 设置一个计时器偶尔处理建立在一个单独的光纤请求。
  3. 通知用户“完成”的构建请求与另一个收集和发布。

这里有一个如何可能被结构化为例[不要来烦测试]:

"use strict"; 

var fs, shell, log, Future; 
// need a collection to store build requests & their status 
var Builds = new Mongo.Collection('builds'); 
var APP_01 = 'app01-andy'; 
var APP_02 = 'app02-andy'; 

// expose routes to client & server 
Router.configure({ 
    layoutTemplate: 'layout' 
}); 
Router.map(function() { 
    this.route('/', 'home'); 
    this.route('downloadApp', { 
    where: 'server' 
    , action: function() { 
     var params = this.request.query, 
      buildId = params.buildId, 
      build, stat; 
     check(buildId, String); // or Mongo Identifier 
     build = Builds.findOne({_id: buildId}); 
     check(build, Match.ObjectIncluding({ 
     file: String, 
     fileName: String 
     })); 
     stat = fs.statSync(build.file); 
     this.response.writeHead(200, { 
     'Content-Type': 'application/vnd.android.package-archive' 
     , 'Content-Length': stat.size 
     , 'Content-Disposition': 'attachment; filename=' + build.fileName 
     }); 
     fs.createReadStream(build.file).pipe(this.response); 
    } 
    }); 
}); 
if (Meteor.isClient) { 

    Session.set('currentBuildId', null); 
    Session.set('submittingBuildRequest', false); 

    Tracker.autorun(function(){ 
    // Whenever there is a current build set, subscribe to it. 
    var buildId = Session.get('currentBuildId'); 
    if (buildId != null){ 
     Meteor.subscribe('build', buildId); 
    } 
    }); 

    Template.home.helpers({ 
    // Use this helper in your template to expose the `state` property (queued, running, success, failed) 
    currentBuild: function() { 
     var buildId = Session.get('currentBuildId'), build; 
     if (buildId == null) return null; 
     build = Builds.findOne({_id: buildId}); 
     if (build == null) return null; 
     return build; 
    } 
    }); 

    Template.home.events({ 
    'submit .app-build-form': function(e) { 
     var target, buildRequest; 
     e.preventDefault(); 
     target = e.target; 

     buildRequest = { 
     appId: target.appId.value 
     , server: target.server.value 
     , db: target.db.value 
     , app: target.app.value 
     }; 
     Session.set('submittingBuildRequest', true); 

     Meteor.call('requestBuild', buildRequest, function(error, buildId) { 
     Session.set('submittingBuildRequest', false); 
     if (error != null){ 
      console.error(error); 
     } else { 
      console.log("buildId=", JSON.stringify(buildId)); 
      Session.set('currentBuildId', buildId); 
     } 
     }); 
    }, 
    'click #sel_app': function(e) { 
     var app = e.target.value; 
     var selDb = $("#sel_db"); 
     if (app === APP_02) { 
     selDb.val('APP_02_DB'); selDb.prop('disabled', true); 
     } 
     else { 
     selDb.prop('disabled', false); 
     } 
    }, 
    'click a.downloadCurrentBuild': function(){ 
     // Alternatively, could create a download url with the "pathFor" template helper 
     Router.go('downloadApp', {buildId: Session.get('currentBuildId')}) 
    } 
    }); 
} 

if (Meteor.isServer) { 
    fs = Npm.require('fs'); 
    shell = Meteor.npmRequire('shelljs'); 
    log = Meteor.npmRequire('winston'); 
    Future = Npm.require('fibers/future'); 

    Meteor.publish({ 
    'build': function(buildId){ 
     check(buildId, String); // or Mongo Identifier 
     return Builds.find({_id: buildId}, {fields: { 
     fileName: false, // don't expose real paths to client 
     file: false 
     }}); 
    } 
    }); 

    Meteor.methods({ 
    'requestBuild': function(buildRequest){ 
     check(buildRequest, { 
     appId: Match.Integer, 
     server: String, // apply additional restrictions 
     db: String, // apply additional restrictions 
     app: Match.OneOf(APP_01, APP_02) 
     }); 

     _.extend(buildRequest, { 
     state: 'queued' 
     // These properties will be set later, just keeping them here for reference 
     //, output: null 
     //, fileName: null 
     //, file: null 
     }); 

     return Builds.insert(buildRequest); 
    } 
    }); 


    var Builder = { 
    // Alternative: Poll the database for new builds 
    //run: function Builder_Run() { 
    // log.info('checking for "queued" builds'); 
    // // may need to change this to `fetch` and then loop manually 
    // Builds.find({state: 'queued'}).forEach(Builder.processBuildRequest); 
    // Meteor.setTimeout(Builder.run, 1000); // tail call to run again (after 1 second) 
    //}, 
    exec: function(cmd){ 
     // Wraps shell.exec so that they don't block the event loop 
     var fut = new Future(); 
     shell.exec(cmd, function(code, output){ 
     fut.return({code: code, output: output}); 
     }); 
     return fut.wait(); 
    }, 
    processBuildRequest: function Builder_processBuildRequest(buildRequest) { 
     console.log('running buildRequest=', JSON.stringify(buildRequest)); 
     Builds.update({_id: buildRequest._id}, { 
     $set: { 
      state: 'running' 
     } 
     }); 

     var branch = null; 
     if (buildRequest.ap === APP_01) { 
     branch = '2.0'; 
     } 
     else if (buildRequest.ap === APP_02) { 
     branch = '1.0'; 
     } 
     else { 
     branch = 'master'; 
     } 

     shell.cd('/project/build/'); 
     Builder.exec('rm -rf ' + buildRequest.app); 
     Builder.exec('git clone -b ' + branch + ' ssh://[email protected]/' + buildRequest.app + '.git'); 
     shell.cd(buildRequest.app + "/app"); 
     //var exec = Builder.exec('./genApp.sh ' + buildRequest.appId + " " + buildRequest.server + " " + buildRequest.db) 
     var exec = Builder.exec('sleep 180'); 
     var output = exec.output; 

     log.info("code=" + exec.code); 
     log.info("output=" + output); 

     var fileName = null; 
     var matches = output.match(/The package copied to (.+apk)/); 
     var file = null; 
     if (matches != null && matches.length > 1) { 
     file = matches[1]; 
     log.info("file=" + file); 
     fileName = file.substring(file.lastIndexOf('/') + 1); 
     } 
     matches = output.match(/BUILD SUCCESSFUL/); 

     if (matches != null && matches.length > 0) { 
     log.info("success"); 
     Builds.update({_id: buildRequest._id}, { 
      $set: { 
      state: 'success', 
      file: file, 
      fileName: fileName, 
      output: output 
      } 
     }); 
     } else { 
     log.info("failed"); 
     Builds.update({_id: buildRequest._id}, { 
      $set: { 
      state: 'failed' 
      } 
     }); 
     } 
    } 
    }; 


    Meteor.startup(function() { 
    // code to run on server at startup 

    // if using polling method 
    //Meteor.setTimeout(Builder.run, 1000); // will poll for new builds every second (unless already running a build) 

    // set up an observe [to run forever] to automatically process new builds. 
    Builds.find({state: 'queued'}).observe({ 
     added: Builder.processBuildRequest 
    }); 
    }); 
}