Fix concurrent requests for KML export
This commit is contained in:
parent
a0d9038e59
commit
38f99c586a
1
NEWS.md
1
NEWS.md
@ -3,6 +3,7 @@
|
|||||||
* Fix parsing of numeric arrays (#88)
|
* Fix parsing of numeric arrays (#88)
|
||||||
* node-pool upgraded to 2.0.3
|
* node-pool upgraded to 2.0.3
|
||||||
* Reduce memory use on KML export
|
* Reduce memory use on KML export
|
||||||
|
* Fix concurrent request for KML exports
|
||||||
* Make temporary dir a configuration setting
|
* Make temporary dir a configuration setting
|
||||||
|
|
||||||
1.3.6 (DD/MM/YY)
|
1.3.6 (DD/MM/YY)
|
||||||
|
@ -46,6 +46,9 @@ var tableCache = LRU({
|
|||||||
maxAge: global.settings.tableCacheMaxAge || 1000*60*10
|
maxAge: global.settings.tableCacheMaxAge || 1000*60*10
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keeps track of what's waiting baking for export
|
||||||
|
var bakingExports = {};
|
||||||
|
|
||||||
app.use(express.bodyParser());
|
app.use(express.bodyParser());
|
||||||
app.enable('jsonp callback');
|
app.enable('jsonp callback');
|
||||||
|
|
||||||
@ -505,7 +508,7 @@ function toCSV(data, res, callback){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Internal function usable by all OGR-driven outputs
|
// Internal function usable by all OGR-driven outputs
|
||||||
function toOGR(dbname, user_id, gcol, sql, skipfields, res, out_format, out_filename, callback) {
|
function toOGR(dbname, user_id, gcol, sql, skipfields, out_format, out_filename, callback) {
|
||||||
var ogr2ogr = 'ogr2ogr'; // FIXME: make configurable
|
var ogr2ogr = 'ogr2ogr'; // FIXME: make configurable
|
||||||
var dbhost = global.settings.db_host;
|
var dbhost = global.settings.db_host;
|
||||||
var dbport = global.settings.db_port;
|
var dbport = global.settings.db_port;
|
||||||
@ -627,7 +630,7 @@ function toSHP(dbname, user_id, gcol, sql, skipfields, filename, res, callback)
|
|||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toOGR(dbname, user_id, gcol, sql, skipfields, res, 'ESRI Shapefile', shapefile, this);
|
toOGR(dbname, user_id, gcol, sql, skipfields, 'ESRI Shapefile', shapefile, this);
|
||||||
},
|
},
|
||||||
function zipAndSendDump(err) {
|
function zipAndSendDump(err) {
|
||||||
if ( err ) throw err;
|
if ( err ) throw err;
|
||||||
@ -706,75 +709,47 @@ function toSHP(dbname, user_id, gcol, sql, skipfields, filename, res, callback)
|
|||||||
function toKML(dbname, user_id, gcol, sql, skipfields, res, callback) {
|
function toKML(dbname, user_id, gcol, sql, skipfields, res, callback) {
|
||||||
var zip = 'zip'; // FIXME: make configurable
|
var zip = 'zip'; // FIXME: make configurable
|
||||||
var tmpdir = global.settings.tmpDir || '/tmp';
|
var tmpdir = global.settings.tmpDir || '/tmp';
|
||||||
var outdirpath = tmpdir + '/sqlapi-kmloutput-' + generateMD5(sql);
|
var reqKey = [ 'kml', dbname, user_id, gcol, generateMD5(sql) ].concat(skipfields).join(':');
|
||||||
var dumpfile = outdirpath + '/cartodb-query.kml';
|
var outdirpath = tmpdir + '/sqlapi-' + reqKey;
|
||||||
|
var dumpfile = outdirpath + ':cartodb-query.kml';
|
||||||
|
|
||||||
// TODO: following tests:
|
// TODO: following tests:
|
||||||
// - fetch same query concurrently
|
|
||||||
// - fetch query with no "the_geom" column
|
// - fetch query with no "the_geom" column
|
||||||
|
|
||||||
|
var baking = bakingExports[reqKey];
|
||||||
|
if ( baking ) {
|
||||||
|
baking.req.push( {cb:callback, res:res} );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
baking = bakingExports[reqKey] = {
|
||||||
|
req: [ {cb:callback, res:res} ]
|
||||||
|
};
|
||||||
|
|
||||||
Step (
|
Step (
|
||||||
|
|
||||||
function createOutDir() {
|
function spawnDumper() {
|
||||||
fs.mkdir(outdirpath, 0777, this);
|
toOGR(dbname, user_id, gcol, sql, skipfields, 'KML', dumpfile, this);
|
||||||
},
|
|
||||||
function spawnDumper(err) {
|
|
||||||
if ( err ) {
|
|
||||||
if ( err.code == 'EEXIST' ) {
|
|
||||||
// TODO: this could mean another request for the same
|
|
||||||
// resource is in progress, in which case we might want
|
|
||||||
// to queue the response to after it's completed...
|
|
||||||
console.log("Reusing existing KML output directory for query: " + sql);
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
toOGR(dbname, user_id, gcol, sql, skipfields, res, 'KML', dumpfile, this);
|
|
||||||
},
|
},
|
||||||
function sendResults(err) {
|
function sendResults(err) {
|
||||||
|
|
||||||
|
//console.log("toOGR completed, have to send result to " + baking.req.length + " requests");
|
||||||
|
|
||||||
if ( ! err ) {
|
if ( ! err ) {
|
||||||
fs.createReadStream(dumpfile).pipe(res);
|
_.each(baking.req, function(r) {
|
||||||
|
fs.createReadStream(dumpfile).pipe(r.res);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// cleanup output dir (should be safe to unlink)
|
delete bakingExports[reqKey];
|
||||||
var topError = err;
|
|
||||||
var next = this;
|
|
||||||
|
|
||||||
//console.log("Cleaning up " + outdirpath);
|
// unlink dump file (sync to avoid race condition)
|
||||||
|
fs.unlinkSync(dumpfile);
|
||||||
|
|
||||||
// Unlink the dir content
|
_.each(baking.req, function(r) {
|
||||||
var unlinkall = function(dir, files, finish) {
|
r.cb(err);
|
||||||
var f = files.shift();
|
|
||||||
if ( ! f ) { finish(null); return; }
|
|
||||||
var fn = dir + '/' + f;
|
|
||||||
fs.unlink(fn, function(err) {
|
|
||||||
if ( err ) {
|
|
||||||
console.log("Unlinking " + fn + ": " + err);
|
|
||||||
finish(err);
|
|
||||||
}
|
|
||||||
else unlinkall(dir, files, finish)
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
fs.readdir(outdirpath, function(err, files) {
|
|
||||||
if ( err ) {
|
|
||||||
if ( err.code != 'ENOENT' ) {
|
|
||||||
next(new Error([topError, err].join('\n')));
|
|
||||||
} else {
|
|
||||||
next(topError);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unlinkall(outdirpath, files, function(err) {
|
|
||||||
fs.rmdir(outdirpath, function(err) {
|
|
||||||
if ( err ) console.log("Removing dir " + path + ": " + err);
|
|
||||||
next(topError);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
function finish(err) {
|
|
||||||
callback(err);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -137,4 +137,29 @@ test('KML format, authenticated', function(done){
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('KML format, unauthenticated, concurrent requests', function(done){
|
||||||
|
var query = querystring.stringify({
|
||||||
|
q: "SELECT 'val', x, y, st_makepoint(x,y,4326) as the_geom FROM generate_series(-180, 180) as x, generate_series(-90,90) y",
|
||||||
|
format: 'kml',
|
||||||
|
filename: 'multi'
|
||||||
|
});
|
||||||
|
|
||||||
|
var waiting = 4;
|
||||||
|
|
||||||
|
for (var i=0; i<waiting; ++i) {
|
||||||
|
assert.response(app, {
|
||||||
|
url: '/api/v1/sql?' + query,
|
||||||
|
headers: {host: 'vizzuality.cartodb.com'},
|
||||||
|
encoding: 'binary',
|
||||||
|
method: 'GET'
|
||||||
|
},{ }, function(res){
|
||||||
|
assert.equal(res.statusCode, 200, res.body);
|
||||||
|
var cd = res.header('Content-Disposition');
|
||||||
|
assert.equal(true, /^attachment/.test(cd), 'KML is not disposed as attachment: ' + cd);
|
||||||
|
assert.equal(true, /filename=multi.kml/gi.test(cd), 'Unexpected KML filename: ' + cd);
|
||||||
|
if ( ! --waiting ) done();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user