2015-01-08 84 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 
    }); 
    }); 
}