1
0
Fork 0

Merge branch 'devel' of https://github.com/triAGENS/ArangoDB into devel

Conflicts:
	arangod/Aql/ExecutionNode.h
This commit is contained in:
Jan Steemann 2014-09-26 20:23:52 +02:00
commit 9e47f3ca27
15 changed files with 336 additions and 32 deletions

View File

@ -28,6 +28,7 @@
#include "Aql/ExecutionBlock.h"
#include "Aql/ExecutionEngine.h"
#include "Basics/StringUtils.h"
#include "Basics/StringBuffer.h"
#include "Basics/json-utilities.h"
#include "HashIndex/hash-index.h"
#include "Utils/Exception.h"
@ -40,6 +41,7 @@ using namespace triagens::aql;
using Json = triagens::basics::Json;
using JsonHelper = triagens::basics::JsonHelper;
using StringBuffer = triagens::basics::StringBuffer;
// -----------------------------------------------------------------------------
// --SECTION-- struct AggregatorGroup
@ -3932,6 +3934,34 @@ size_t ScatterBlock::skipSomeForShard (size_t atLeast, size_t atMost, std::strin
// --SECTION-- class RemoteBlock
// -----------------------------------------------------------------------------
////////////////////////////////////////////////////////////////////////////////
/// @brief timeout
////////////////////////////////////////////////////////////////////////////////
double const RemoteBlock::defaultTimeOut = 3600.0;
////////////////////////////////////////////////////////////////////////////////
/// @brief local helper to throw an exception if a HTTP request went wrong
////////////////////////////////////////////////////////////////////////////////
static void throwExceptionAfterBadSyncRequest (ClusterCommResult* res) {
if (res->status == CL_COMM_TIMEOUT) {
// No reply, we give up:
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_CLUSTER_TIMEOUT,
"timeout in cluster AQL operation");
}
if (res->status == CL_COMM_ERROR) {
// This could be a broken connection or an Http error:
if (res->result == nullptr || ! res->result->isComplete()) {
// there is no result
THROW_ARANGO_EXCEPTION_MESSAGE(TRI_ERROR_CLUSTER_CONNECTION_LOST,
"lost connection within cluster");
}
// In this case a proper HTTP error was reported by the DBserver,
THROW_ARANGO_EXCEPTION(TRI_ERROR_CLUSTER_AQL_COMMUNICATION);
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief initialize
////////////////////////////////////////////////////////////////////////////////
@ -3969,15 +3999,103 @@ int RemoteBlock::shutdown () {
AqlItemBlock* RemoteBlock::getSome (size_t atLeast,
size_t atMost) {
// For every call we simply forward via HTTP
ClusterComm* cc = ClusterComm::instance();
std::unique_ptr<ClusterCommResult> res;
// Later, we probably want to set these sensibly:
ClientTransactionID const clientTransactionId = "AQL";
CoordTransactionID const coordTransactionId = 1;
Json body(Json::Array, 2);
body("atLeast", Json(static_cast<double>(atLeast)))
("atMost", Json(static_cast<double>(atMost)));
std::string bodyString(body.toString());
std::map<std::string, std::string> headers;
res.reset(cc->syncRequest(clientTransactionId,
coordTransactionId,
_server,
rest::HttpRequest::HTTP_REQUEST_PUT,
std::string("/_db/")
+ _engine->getTransaction()->vocbase()->_name
+ "/_api/aql/getSome/" + _queryId,
bodyString,
headers,
3600.0));
throwExceptionAfterBadSyncRequest(res.get());
// If we get here, then res->result is the response which will be
// a serialized AqlItemBlock:
StringBuffer const& responseBodyBuf(res->result->getBody());
Json responseBodyJson(TRI_UNKNOWN_MEM_ZONE,
TRI_JsonString(TRI_UNKNOWN_MEM_ZONE,
responseBodyBuf.begin()));
if (JsonHelper::getBooleanValue(responseBodyJson.json(), "exhausted", true)) {
return nullptr;
}
else {
auto items = new triagens::aql::AqlItemBlock(responseBodyJson);
return items;
}
}
////////////////////////////////////////////////////////////////////////////////
/// @brief skipSome
////////////////////////////////////////////////////////////////////////////////
ClusterCommResult* RemoteBlock::sendRequest (
rest::HttpRequest::HttpRequestType type,
std::string urlPart,
std::string const& body) {
ClusterComm* cc = ClusterComm::instance();
// Later, we probably want to set these sensibly:
ClientTransactionID const clientTransactionId = "AQL";
CoordTransactionID const coordTransactionId = 1;
std::map<std::string, std::string> headers;
return cc->syncRequest(clientTransactionId,
coordTransactionId,
_server,
type,
std::string("/_db/")
+ _engine->getTransaction()->vocbase()->_name
+ urlPart + _queryId,
body,
headers,
defaultTimeOut);
}
size_t RemoteBlock::skipSome (size_t atLeast, size_t atMost) {
return 0;
// For every call we simply forward via HTTP
Json body(Json::Array, 2);
body("atLeast", Json(static_cast<double>(atLeast)))
("atMost", Json(static_cast<double>(atMost)));
std::string bodyString(body.toString());
std::unique_ptr<ClusterCommResult> res;
res.reset(sendRequest(rest::HttpRequest::HTTP_REQUEST_PUT,
"/_api/aql/skipSome/",
bodyString));
throwExceptionAfterBadSyncRequest(res.get());
// If we get here, then res->result is the response which will be
// a serialized AqlItemBlock:
StringBuffer const& responseBodyBuf(res->result->getBody());
Json responseBodyJson(TRI_UNKNOWN_MEM_ZONE,
TRI_JsonString(TRI_UNKNOWN_MEM_ZONE,
responseBodyBuf.begin()));
if (JsonHelper::getBooleanValue(responseBodyJson.json(), "error", true)) {
THROW_ARANGO_EXCEPTION(TRI_ERROR_CLUSTER_AQL_COMMUNICATION);
}
size_t skipped = JsonHelper::getNumericValue<size_t>(responseBodyJson.json(),
"skipped", 0);
return skipped;
}
////////////////////////////////////////////////////////////////////////////////

View File

@ -39,6 +39,7 @@
#include "Utils/AqlTransaction.h"
#include "Utils/transactions.h"
#include "Utils/V8TransactionContext.h"
#include "Cluster/ClusterComm.h"
namespace triagens {
namespace aql {
@ -1638,20 +1639,33 @@ public:
class RemoteBlock : public ExecutionBlock {
////////////////////////////////////////////////////////////////////////////////
/// @brief constructors/destructors
////////////////////////////////////////////////////////////////////////////////
public:
RemoteBlock (ExecutionEngine* engine,
RemoteNode const* en,
std::string const& server,
std::string const& ownName)
std::string const& ownName,
std::string const& queryId)
: ExecutionBlock(engine, en),
_server(server),
_ownName(ownName) {
_ownName(ownName),
_queryId(queryId) {
TRI_ASSERT(! queryId.empty());
}
~RemoteBlock () {
}
////////////////////////////////////////////////////////////////////////////////
/// @brief timeout
////////////////////////////////////////////////////////////////////////////////
static double const defaultTimeOut;
////////////////////////////////////////////////////////////////////////////////
/// @brief initialize
////////////////////////////////////////////////////////////////////////////////
@ -1702,19 +1716,35 @@ public:
int64_t remaining () final;
////////////////////////////////////////////////////////////////////////////////
/// @brief our server, can be like "shard:S1000" or like "server:Claus"
/// @brief internal method to send a request
////////////////////////////////////////////////////////////////////////////////
private:
std::string _server;
triagens::arango::ClusterCommResult* sendRequest (
rest::HttpRequest::HttpRequestType type,
std::string urlPart,
std::string const& body);
////////////////////////////////////////////////////////////////////////////////
/// @brief our server, can be like "shard:S1000" or like "server:Claus"
////////////////////////////////////////////////////////////////////////////////
std::string _server;
////////////////////////////////////////////////////////////////////////////////
/// @brief our own identity, in case of the coordinator this is empty,
/// in case of the DBservers, this is the shard ID as a string
////////////////////////////////////////////////////////////////////////////////
std::string _ownName;
////////////////////////////////////////////////////////////////////////////////
/// @brief the ID of the query on the server as a string
////////////////////////////////////////////////////////////////////////////////
std::string _queryId;
};
} // namespace triagens::aql

View File

@ -451,7 +451,8 @@ struct CoordinatorInstanciator : public WalkerWorker<ExecutionNode> {
// now we'll create a remote node for each shard and add it to the gather node
auto&& shardIds = static_cast<GatherNode const*>((*en))->collection()->shardIds();
for (auto shardId : shardIds) {
ExecutionBlock* r = new RemoteBlock(engine.get(), remoteNode, "shard:" + shardId, "");
// TODO: pass actual queryId into RemoteBlock
ExecutionBlock* r = new RemoteBlock(engine.get(), remoteNode, "shard:" + shardId, "", "" /*TODO*/);
try {
engine.get()->addBlock(r);

View File

@ -114,6 +114,22 @@ namespace triagens {
return _query;
}
////////////////////////////////////////////////////////////////////////////////
/// @brief initializeCursor, could be called multiple times
////////////////////////////////////////////////////////////////////////////////
int initializeCursor (AqlItemBlock* items, size_t pos) {
return _root->initializeCursor(items, pos);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief shutdown, will be called exactly once for the whole query
////////////////////////////////////////////////////////////////////////////////
int shutdown () {
return _root->shutdown();
}
////////////////////////////////////////////////////////////////////////////////
/// @brief getSome
////////////////////////////////////////////////////////////////////////////////
@ -122,6 +138,14 @@ namespace triagens {
return _root->getSome(atLeast, atMost);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief skipSome
////////////////////////////////////////////////////////////////////////////////
size_t skipSome (size_t atLeast, size_t atMost) {
return _root->skipSome(atLeast, atMost);
}
////////////////////////////////////////////////////////////////////////////////
/// @brief getOne
////////////////////////////////////////////////////////////////////////////////

View File

@ -93,7 +93,7 @@ void ExecutionNode::validateType (int type) {
}
}
void ExecutionNode::getSortElements(SortElementVector elements,
void ExecutionNode::getSortElements(SortElementVector& elements,
ExecutionPlan* plan,
triagens::basics::Json const& oneNode,
char const* which)

View File

@ -505,14 +505,14 @@ namespace triagens {
static Variable* varFromJson (Ast* ast,
triagens::basics::Json const& base,
const char *variableName,
char const* variableName,
bool optional = false);
////////////////////////////////////////////////////////////////////////////////
/// @brief factory for sort Elements from json.
////////////////////////////////////////////////////////////////////////////////
static void getSortElements(SortElementVector elements,
static void getSortElements (SortElementVector& elements,
ExecutionPlan* plan,
triagens::basics::Json const& oneNode,
char const* which);
@ -1697,7 +1697,7 @@ namespace triagens {
/// @brief get Variables Used Here including ASC/DESC
////////////////////////////////////////////////////////////////////////////////
SortElementVector getElements () const {
SortElementVector const & getElements () const {
return _elements;
}
@ -2866,11 +2866,11 @@ namespace triagens {
/// @brief get Variables Used Here including ASC/DESC
////////////////////////////////////////////////////////////////////////////////
SortElementVector getElements () const {
SortElementVector const & getElements () const {
return _elements;
}
void setElements (SortElementVector const src) {
void setElements (SortElementVector const & src) {
_elements = src;
}
@ -2890,7 +2890,6 @@ namespace triagens {
return _collection;
}
private:
////////////////////////////////////////////////////////////////////////////////

View File

@ -381,7 +381,8 @@ void RestAqlHandler::deleteQuery (std::string const& idString) {
////////////////////////////////////////////////////////////////////////////////
/// @brief PUT method for /_api/aql/<operation>/<queryId>, this is using
/// the part of the cursor API with side effects.
/// <operation>: can be "getSome" or "skip".
/// <operation>: can be "getSome" or "skip" or "initializeCursor" or
/// "shutdown".
/// The body must be a Json with the following attributes:
/// For the "getSome" operation one has to give:
/// "atLeast":
@ -395,6 +396,19 @@ void RestAqlHandler::deleteQuery (std::string const& idString) {
/// AqlItemBlock.
/// If "atLeast" is not given it defaults to 1, if "atMost" is not
/// given it defaults to ExecutionBlock::DefaultBatchSize.
/// For the "skipSome" operation one has to give:
/// "atLeast":
/// "atMost": both must be positive integers, the cursor skips never
/// more than "atMost" items and tries to skip at least
/// "atLeast". Note that it is possible to skip fewer than
/// "atLeast", for example if there are only fewer items
/// left. However, the implementation may skip fewer items
/// than "atLeast" for internal reasons, for example to avoid
/// excessive copying. The result is a JSON object with a
/// single attribute "skipped" containing the number of
/// skipped items.
/// If "atLeast" is not given it defaults to 1, if "atMost" is not
/// given it defaults to ExecutionBlock::DefaultBatchSize.
/// For the "skip" operation one should give:
/// "number": must be a positive integer, the cursor skips as many items,
/// possibly exhausting the cursor.
@ -402,6 +416,13 @@ void RestAqlHandler::deleteQuery (std::string const& idString) {
/// "errorMessage" (if applicable) and "exhausted" (boolean)
/// to indicate whether or not the cursor is exhausted.
/// If "number" is not given it defaults to 1.
/// For the "initializeCursor" operation, one has to bind the following
/// attributes:
/// "items": This is a serialised AqlItemBlock with usually only one row
/// and the correct number of columns.
/// "pos": The number of the row in "items" to take, usually 0.
/// For the "shutdown" operation no additional arguments are required and
/// an empty JSON object in the body is OK.
////////////////////////////////////////////////////////////////////////////////
void RestAqlHandler::useQuery (std::string const& operation,
@ -447,6 +468,24 @@ void RestAqlHandler::useQuery (std::string const& operation,
}
}
}
else if (operation == "getSome") {
auto atLeast = JsonHelper::getNumericValue<uint64_t>(queryJson.json(),
"atLeast", 1);
auto atMost = JsonHelper::getNumericValue<uint64_t>(queryJson.json(),
"atMost", ExecutionBlock::DefaultBatchSize);
size_t skipped;
try {
skipped = query->engine()->skipSome(atLeast, atMost);
}
catch (...) {
_queryRegistry->close(vocbase, qId);
generateError(HttpResponse::SERVER_ERROR, TRI_ERROR_HTTP_SERVER_ERROR,
"skipSome lead to an exception");
return;
}
answerBody("skipped", Json(static_cast<double>(skipped)))
("error", Json(false));
}
else if (operation == "skip") {
auto number = JsonHelper::getNumericValue<uint64_t>(queryJson.json(),
"number", 1);
@ -462,6 +501,38 @@ void RestAqlHandler::useQuery (std::string const& operation,
return;
}
}
else if (operation == "initializeCursor") {
auto pos = JsonHelper::getNumericValue<size_t>(queryJson.json(),
"pos", 0);
std::unique_ptr<AqlItemBlock> items;
int res;
try {
items.reset(new AqlItemBlock(queryJson.get("items")));
res = query->engine()->initializeCursor(items.get(), pos);
}
catch (...) {
_queryRegistry->close(vocbase, qId);
generateError(HttpResponse::SERVER_ERROR, TRI_ERROR_HTTP_SERVER_ERROR,
"initializeCursor lead to an exception");
return;
}
answerBody("error", res == TRI_ERROR_NO_ERROR ? Json(false) : Json(true))
("code", Json(static_cast<double>(res)));
}
else if (operation == "shutdown") {
int res;
try {
res = query->engine()->shutdown();
}
catch (...) {
_queryRegistry->close(vocbase, qId);
generateError(HttpResponse::SERVER_ERROR, TRI_ERROR_HTTP_SERVER_ERROR,
"shutdown lead to an exception");
return;
}
answerBody("error", res == TRI_ERROR_NO_ERROR ? Json(false) : Json(true))
("code", Json(static_cast<double>(res)));
}
else {
_queryRegistry->close(vocbase, qId);
generateError(HttpResponse::NOT_FOUND, TRI_ERROR_HTTP_NOT_FOUND);

View File

@ -860,7 +860,7 @@ void RestReplicationHandler::handleTrampolineCoordinator () {
}
if (res->status == CL_COMM_ERROR) {
// This could be a broken connection or an Http error:
if (res->result == 0 || !res->result->isComplete()) {
if (res->result == nullptr || !res->result->isComplete()) {
// there is no result
delete res;
generateError(HttpResponse::BAD, TRI_ERROR_CLUSTER_CONNECTION_LOST,

View File

@ -144,6 +144,7 @@
"ERROR_CLUSTER_ONLY_ON_COORDINATOR" : { "code" : 1471, "message" : "this operation is only valid on a coordinator in a cluster" },
"ERROR_CLUSTER_READING_PLAN_AGENCY" : { "code" : 1472, "message" : "error reading Plan in agency" },
"ERROR_CLUSTER_COULD_NOT_TRUNCATE_COLLECTION" : { "code" : 1473, "message" : "could not truncate collection" },
"ERROR_CLUSTER_AQL_COMMUNICATION" : { "code" : 1474, "message" : "error in cluster internal communication for AQL" },
"ERROR_QUERY_KILLED" : { "code" : 1500, "message" : "query killed" },
"ERROR_QUERY_PARSE" : { "code" : 1501, "message" : "%s" },
"ERROR_QUERY_EMPTY" : { "code" : 1502, "message" : "query is empty" },

View File

@ -144,6 +144,7 @@
"ERROR_CLUSTER_ONLY_ON_COORDINATOR" : { "code" : 1471, "message" : "this operation is only valid on a coordinator in a cluster" },
"ERROR_CLUSTER_READING_PLAN_AGENCY" : { "code" : 1472, "message" : "error reading Plan in agency" },
"ERROR_CLUSTER_COULD_NOT_TRUNCATE_COLLECTION" : { "code" : 1473, "message" : "could not truncate collection" },
"ERROR_CLUSTER_AQL_COMMUNICATION" : { "code" : 1474, "message" : "error in cluster internal communication for AQL" },
"ERROR_QUERY_KILLED" : { "code" : 1500, "message" : "query killed" },
"ERROR_QUERY_PARSE" : { "code" : 1501, "message" : "%s" },
"ERROR_QUERY_EMPTY" : { "code" : 1502, "message" : "query is empty" },

View File

@ -86,6 +86,7 @@ var _ = require("underscore");
var testFuncs = {};
var print = require("internal").print;
var time = require("internal").time;
var fs = require("fs");
var download = require("internal").download;
var wait = require("internal").wait;
@ -398,17 +399,34 @@ function runThere (options, instanceInfo, file) {
function executeAndWait (cmd, args) {
var pid = executeExternal(cmd, args);
var startTime = time();
var res = statusExternal(pid, true);
var deltaTime = time() - startTime;
if (res.status === "TERMINATED") {
print("Finished: " + res.status + " Exitcode: " + res.exit + " Time Elapsed: " + deltaTime);
if (res.exit === 0) {
return { status: true, message: "" };
return { status: true, message: "", duration: deltaTime};
}
else {
return { status: false, message: "exit code was " + res.exit};
return { status: false, message: "exit code was " + res.exit, duration: deltaTime};
}
}
else if (res.status === "ABORTED") {
print("Finished: " + res.status + " Signal: " + res.signal + " Time Elapsed: " + deltaTime);
return {
status: false,
message: "irregular termination: " + res.status + " Exit-Signal: " + res.signal,
duration: deltaTime
};
}
else {
return { status: false, message: "irregular termination: " + res.status};
print("Finished: " + res.status + " Exitcode: " + res.exit + " Time Elapsed: " + deltaTime);
return {
status: false,
message: "irregular termination: " + res.status + " Exit-Code: " + res.exit,
duration: deltaTime
};
}
}
@ -671,19 +689,35 @@ function rubyTests (options, ssl) {
"--format", "d", "--require", tmpname,
fs.join("UnitTests","HttpInterface",n)];
var pid = executeExternal("rspec", args);
var startTime = time();
var r = statusExternal(pid, true);
var deltaTime = time() - startTime;
if (r.status === "TERMINATED") {
print("Finished: " + r.status + " Signal: " + r.exit + " Time Elapsed: " + deltaTime);
if (r.exit === 0) {
result[n] = { status: true, message: "" };
result[n] = { status: true, message: "", duration: deltaTime };
}
else {
result[n] = { status: false, message: "exit code was " + r.exit};
result[n] = { status: false, message: " exit code was " + r.exit, duration: deltaTime};
}
}
else if (r.status === "ABORTED") {
print("Finished: " + r.status + " Signal: " + r.exit + " Time Elapsed: " + deltaTime);
result[n] = {
status: false,
message: "irregular termination: " + r.status + " Exit-Signal: " + r.signal,
duration: deltaTime
};
}
else {
result[n] = { status: false, message: "irregular termination: " + r.status};
print("Finished: " + r.status + " Exit Status: " + r.exit + " Time Elapsed: " + deltaTime);
result[n] = {
status: false,
message: "irregular termination: " + r.status + " Exit-code: " + r.exit,
duration: deltaTime
};
}
if (r.status === false && !options.force) {
break;
}

View File

@ -180,6 +180,7 @@ ERROR_CLUSTER_UNSUPPORTED,1470,"unsupported operation or parameter","Will be rai
ERROR_CLUSTER_ONLY_ON_COORDINATOR,1471,"this operation is only valid on a coordinator in a cluster","Will be raised if there is an attempt to run a coordinator-only operation on a different type of node."
ERROR_CLUSTER_READING_PLAN_AGENCY,1472,"error reading Plan in agency","Will be raised if a coordinator or DBserver cannot read the Plan in the agency."
ERROR_CLUSTER_COULD_NOT_TRUNCATE_COLLECTION,1473,"could not truncate collection","Will be raised if a coordinator cannot truncate all shards of a cluster collection."
ERROR_CLUSTER_AQL_COMMUNICATION,1474,"error in cluster internal communication for AQL","Will be raised if the internal communication of the cluster for AQL produces an error."
################################################################################
## ArangoDB query errors

View File

@ -140,6 +140,7 @@ void TRI_InitialiseErrorMessages (void) {
REG_ERROR(ERROR_CLUSTER_ONLY_ON_COORDINATOR, "this operation is only valid on a coordinator in a cluster");
REG_ERROR(ERROR_CLUSTER_READING_PLAN_AGENCY, "error reading Plan in agency");
REG_ERROR(ERROR_CLUSTER_COULD_NOT_TRUNCATE_COLLECTION, "could not truncate collection");
REG_ERROR(ERROR_CLUSTER_AQL_COMMUNICATION, "error in cluster internal communication for AQL");
REG_ERROR(ERROR_QUERY_KILLED, "query killed");
REG_ERROR(ERROR_QUERY_PARSE, "%s");
REG_ERROR(ERROR_QUERY_EMPTY, "query is empty");

View File

@ -341,6 +341,9 @@
/// - 1473: @LIT{could not truncate collection}
/// Will be raised if a coordinator cannot truncate all shards of a cluster
/// collection.
/// - 1474: @LIT{error in cluster internal communication for AQL}
/// Will be raised if the internal communication of the cluster for AQL
/// produces an error.
/// - 1500: @LIT{query killed}
/// Will be raised when a running query is killed by an explicit admin
/// command.
@ -1996,6 +1999,17 @@ void TRI_InitialiseErrorMessages (void);
#define TRI_ERROR_CLUSTER_COULD_NOT_TRUNCATE_COLLECTION (1473)
////////////////////////////////////////////////////////////////////////////////
/// @brief 1474: ERROR_CLUSTER_AQL_COMMUNICATION
///
/// error in cluster internal communication for AQL
///
/// Will be raised if the internal communication of the cluster for AQL
/// produces an error.
////////////////////////////////////////////////////////////////////////////////
#define TRI_ERROR_CLUSTER_AQL_COMMUNICATION (1474)
////////////////////////////////////////////////////////////////////////////////
/// @brief 1500: ERROR_QUERY_KILLED
///

View File

@ -31,7 +31,9 @@ function resultsToXml(results, baseName) {
.text(xmlEscape(String(attrs[a]))).text("\"");
}
close && this.text("/");
if (close) {
this.text("/");
}
this.text(">\n");
return this;
@ -64,6 +66,13 @@ function resultsToXml(results, baseName) {
}
}
if ((!results[testrun][test].status) &&
results[testrun][test].hasOwnProperty('message'))
{
xml.elem("failure");
xml.text('<![CDATA[' + JSON.stringify(results[testrun][test].message) + ']]>\n');
xml.elem("/failure");
}
xml.elem("/testsuite");
var fn = baseName + testrun.replace(/\//g, '_') + '_' + test.replace(/\//g, '_') + ".xml";
//print('Writing: '+ fn);