mirror of
https://github.com/open62541/open62541.git
synced 2026-10-16 20:33:18 +08:00
feat(driver): close file handles without activity after a timeout
The activity-timeout parameter closes handles without a Method call like Close, e.g. for the ActivityTimeout of a TrustList (Part 12 7.8.2.1).
This commit is contained in:
committed by
Andreas Ebner
parent
0c253ab297
commit
90effffe86
@@ -131,6 +131,14 @@ driverParam(FileTransferDriver *ftd, const char *key, const UA_DataType *type,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Zero disables a timer. Otherwise the timer needs at least one DateTime tick
|
||||
* and an Int64 tick count (rejects NaN and infinity). */
|
||||
static UA_Boolean
|
||||
validInterval(UA_Double ms) {
|
||||
return ms == 0.0 || (ms * UA_DATETIME_MSEC >= 1.0 &&
|
||||
ms <= (UA_Double)UA_INT64_MAX / UA_DATETIME_MSEC);
|
||||
}
|
||||
|
||||
static UA_StatusCode
|
||||
configureDriver(FileTransferDriver *ftd) {
|
||||
UA_StatusCode res = UA_STATUSCODE_GOOD;
|
||||
@@ -174,13 +182,14 @@ configureDriver(FileTransferDriver *ftd) {
|
||||
driverParam(ftd, "refresh-interval", &UA_TYPES[UA_TYPES_DOUBLE], &res);
|
||||
if(interval)
|
||||
ftd->config.refreshInterval = *interval;
|
||||
const UA_Double *timeout = (const UA_Double*)
|
||||
driverParam(ftd, "activity-timeout", &UA_TYPES[UA_TYPES_DOUBLE], &res);
|
||||
if(timeout)
|
||||
ftd->config.activityTimeout = *timeout;
|
||||
if(res != UA_STATUSCODE_GOOD)
|
||||
return res;
|
||||
/* Zero disables the periodic refresh. Otherwise the timer needs at least
|
||||
* one DateTime tick and an Int64 tick count (rejects NaN and infinity). */
|
||||
UA_Double ri = ftd->config.refreshInterval;
|
||||
if(ri != 0.0 && !(ri * UA_DATETIME_MSEC >= 1.0 &&
|
||||
ri <= (UA_Double)UA_INT64_MAX / UA_DATETIME_MSEC))
|
||||
if(!validInterval(ftd->config.refreshInterval) ||
|
||||
!validInterval(ftd->config.activityTimeout))
|
||||
return UA_STATUSCODE_BADINVALIDARGUMENT;
|
||||
if(!backendComplete(&ftd->backend, !ftd->root->isDirectory, ftd->config.readOnly))
|
||||
return UA_STATUSCODE_BADINVALIDARGUMENT;
|
||||
@@ -250,6 +259,10 @@ startDriver(UA_Driver *drv) {
|
||||
ftd->config.refreshInterval > 0)
|
||||
res = UA_Server_addRepeatedCallback(drv->server, refreshCallback, ftd,
|
||||
ftd->config.refreshInterval, &ftd->refreshCallbackId);
|
||||
if(res == UA_STATUSCODE_GOOD && ftd->config.activityTimeout > 0)
|
||||
res = UA_Server_addRepeatedCallback(drv->server, sweepIdleHandles, ftd,
|
||||
ftd->config.activityTimeout,
|
||||
&ftd->sweepCallbackId);
|
||||
if(res != UA_STATUSCODE_GOOD)
|
||||
FileTransferDriver_stop(drv);
|
||||
return res;
|
||||
@@ -283,6 +296,10 @@ FileTransferDriver_stop(UA_Driver *drv) {
|
||||
UA_Server_removeCallback(drv->server, ftd->refreshCallbackId);
|
||||
ftd->refreshCallbackId = 0;
|
||||
}
|
||||
if(ftd->sweepCallbackId) {
|
||||
UA_Server_removeCallback(drv->server, ftd->sweepCallbackId);
|
||||
ftd->sweepCallbackId = 0;
|
||||
}
|
||||
if(wasStarted)
|
||||
ZIP_ITER(FTEntriesById, &ftd->entriesByNodeId, unbindEntryMethods, NULL);
|
||||
|
||||
@@ -528,6 +545,8 @@ UA_FileTransferDriver_getHandleInfo(UA_Driver *driver, const UA_NodeId fileNodeI
|
||||
UA_EventLoop *el = UA_Server_getConfig(driver->server)->eventLoop;
|
||||
el->lock(el);
|
||||
FTHandle *h = findObjectHandle(driver, &fileNodeId, sessionId, fileHandle);
|
||||
if(h)
|
||||
touchFTHandle(driver->server, h);
|
||||
if(h && mode)
|
||||
*mode = h->mode;
|
||||
if(h && backendHandle)
|
||||
|
||||
@@ -175,6 +175,13 @@ newHandleId(FileTransferDriver *ftd) {
|
||||
return ftd->nextHandle;
|
||||
}
|
||||
|
||||
/* A Method call on the handle restarts its activity-timeout */
|
||||
void
|
||||
touchFTHandle(UA_Server *server, FTHandle *h) {
|
||||
UA_EventLoop *el = UA_Server_getConfig(server)->eventLoop;
|
||||
h->lastActivity = el->dateTime_nowMonotonic(el);
|
||||
}
|
||||
|
||||
/* Close the backend handle and release the handle. Removes zombie nodes once
|
||||
* their last handle is closed. */
|
||||
UA_StatusCode
|
||||
@@ -225,6 +232,29 @@ closeSessionHandles(UA_Server *server, FileTransferDriver *ftd,
|
||||
closeFTHandle(server, h);
|
||||
}
|
||||
|
||||
static void *
|
||||
findIdleHandle(void *context, FTHandle *h) {
|
||||
return h->lastActivity <= *(UA_DateTime*)context ? h : NULL;
|
||||
}
|
||||
|
||||
/* Close the handles without a Method call within the activity-timeout like
|
||||
* Close. The search starts again after each removal. */
|
||||
void
|
||||
sweepIdleHandles(UA_Server *server, void *driver) {
|
||||
FileTransferDriver *ftd = (FileTransferDriver*)driver;
|
||||
UA_EventLoop *el = UA_Server_getConfig(server)->eventLoop;
|
||||
UA_DateTime limit = el->dateTime_nowMonotonic(el) -
|
||||
(UA_DateTime)(ftd->config.activityTimeout * UA_DATETIME_MSEC);
|
||||
FTHandle *h;
|
||||
while((h = (FTHandle*)ZIP_ITER(FTHandlesById, &ftd->handlesById,
|
||||
findIdleHandle, &limit))) {
|
||||
UA_LOG_INFO(UA_Server_getConfig(server)->logging, UA_LOGCATEGORY_SERVER,
|
||||
"FileTransfer: Closing the idle FileHandle %u of %N",
|
||||
(unsigned)h->handle, h->file->nodeId);
|
||||
closeFTHandle(server, h);
|
||||
}
|
||||
}
|
||||
|
||||
/**************************************
|
||||
* FileType Method Callbacks
|
||||
**************************************/
|
||||
@@ -242,6 +272,7 @@ resolveHandle(FileTransferDriver *ftd, const UA_NodeId *sessionId,
|
||||
FTHandle *h = findFTHandle(ftd, sessionId, handleId);
|
||||
if(!h || !UA_NodeId_equal(&h->file->nodeId, objectId))
|
||||
return UA_STATUSCODE_BADINVALIDARGUMENT;
|
||||
touchFTHandle(ftd->driver.server, h);
|
||||
*outHandle = h;
|
||||
return UA_STATUSCODE_GOOD;
|
||||
}
|
||||
@@ -310,6 +341,7 @@ openFileHandle(UA_Server *server, FTEntry *node,
|
||||
h->file = node;
|
||||
h->mode = mode;
|
||||
h->backendHandle = backendHandle;
|
||||
touchFTHandle(server, h);
|
||||
ZIP_INSERT(FTHandlesById, &ftd->handlesById, h);
|
||||
ZIP_INSERT(FTHandlesBySession, &ftd->handlesBySession, h);
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ typedef struct FTHandle {
|
||||
FTEntry *file;
|
||||
UA_Byte mode; /* UA_OPENFILEMODE_* bit mask, matching the Open Method Byte */
|
||||
UA_UInt32 backendHandle;
|
||||
UA_DateTime lastActivity; /* Monotonic, for the activity-timeout */
|
||||
} FTHandle;
|
||||
|
||||
ZIP_HEAD(FTEntriesById, FTEntry);
|
||||
@@ -87,6 +88,7 @@ typedef struct {
|
||||
UA_UInt16 maxHandlesPerSession;
|
||||
UA_UInt16 maxHandlesPerFile;
|
||||
UA_UInt32 maxReadLength;
|
||||
UA_Double activityTimeout;
|
||||
} FTConfig;
|
||||
|
||||
/* One driver owns one backend and one root Object for its entire lifetime. */
|
||||
@@ -96,6 +98,7 @@ struct FileTransferDriver {
|
||||
FTConfig config;
|
||||
FTEntry *root;
|
||||
UA_UInt64 refreshCallbackId;
|
||||
UA_UInt64 sweepCallbackId; /* Closes idle handles */
|
||||
UA_StatusCode refreshResult; /* Of the last periodic refresh */
|
||||
UA_Boolean scanReported; /* lastScan is set; reset on start */
|
||||
FTScanSummary lastScan; /* Of the last refresh of the root */
|
||||
@@ -195,6 +198,8 @@ UA_StatusCode openFileHandle(UA_Server *server, FTEntry *node,
|
||||
UA_Byte mode, void *openContext,
|
||||
FTHandle **outHandle);
|
||||
UA_StatusCode closeFTHandle(UA_Server *server, FTHandle *handle);
|
||||
void touchFTHandle(UA_Server *server, FTHandle *handle);
|
||||
void sweepIdleHandles(UA_Server *server, void *driver);
|
||||
void closeDriverHandles(UA_Server *server, FileTransferDriver *ftd);
|
||||
void closeSessionHandles(UA_Server *server, FileTransferDriver *ftd,
|
||||
const UA_NodeId *sessionId);
|
||||
|
||||
@@ -233,6 +233,12 @@ struct UA_FileTransferBackend {
|
||||
* 0:max-open-handles-per-file [UInt16]
|
||||
* Open-handle limit per file (default: 16). Either handle limit makes
|
||||
* Open return Bad_ResourceUnavailable when reached.
|
||||
* 0:activity-timeout [Double]
|
||||
* Close handles without a Method call for longer than this many
|
||||
* milliseconds like Close, e.g. for the ActivityTimeout of a TrustList
|
||||
* (Part 12, 7.8.2.1). The FileType Methods, openHandle and getHandleInfo
|
||||
* count as calls. Idle handles are closed within twice the timeout. 0
|
||||
* disables it (default); otherwise at least 0.0001.
|
||||
* 0:max-read-length [UInt32]
|
||||
* Maximum bytes per Read or Write (default, also when zero: 1048576).
|
||||
* Published as MaxByteStringLength. Reads are capped at this length;
|
||||
|
||||
@@ -5427,6 +5427,111 @@ START_TEST(directoryLifetimeAndAutomaticRefresh) {
|
||||
UA_NodeId_clear(&file);
|
||||
} END_TEST
|
||||
|
||||
static void
|
||||
setActivityTimeout(UA_Driver *driver, UA_Double timeout) {
|
||||
ck_assert_uint_eq(UA_KeyValueMap_setScalar(&driver->params,
|
||||
UA_QUALIFIEDNAME(0, "activity-timeout"), &timeout,
|
||||
&UA_TYPES[UA_TYPES_DOUBLE]), UA_STATUSCODE_GOOD);
|
||||
}
|
||||
|
||||
static void
|
||||
advanceClock(UA_UInt32 ms) {
|
||||
UA_fakeSleep(ms);
|
||||
UA_Server_run_iterate(server_ft, false);
|
||||
}
|
||||
|
||||
/* Handles without a Method call for longer than the activity-timeout close
|
||||
* like with Close */
|
||||
START_TEST(activityTimeoutClosesIdleHandles) {
|
||||
UA_Server_getConfig(server_ft)->tcpEnabled = false;
|
||||
ck_assert_uint_eq(UA_Server_run_startup(server_ft), UA_STATUSCODE_GOOD);
|
||||
UA_FileTransferBackend backend = memBackendWithFile("f.bin", "content");
|
||||
UA_NodeId file;
|
||||
UA_Driver *driver = newTestFile(server_ft, &backend.file, "Timed", &file);
|
||||
setActivityTimeout(driver, 100);
|
||||
ck_assert_uint_eq(UA_Server_addDriver(server_ft, driver), UA_STATUSCODE_GOOD);
|
||||
|
||||
/* Method calls keep the handle open */
|
||||
UA_UInt32 h = callOpen(file, UA_OPENFILEMODE_READ, UA_STATUSCODE_GOOD);
|
||||
for(size_t i = 0; i < 4; i++) {
|
||||
advanceClock(60);
|
||||
callGetPosition(file, h);
|
||||
}
|
||||
/* So do the calls of subtype Methods */
|
||||
const UA_NodeId *session = &server_ft->adminSession.sessionId;
|
||||
for(size_t i = 0; i < 3; i++) {
|
||||
advanceClock(60);
|
||||
ck_assert_uint_eq(UA_FileTransferDriver_getHandleInfo(driver, file, session,
|
||||
h, NULL, NULL), UA_STATUSCODE_GOOD);
|
||||
}
|
||||
ck_assert_uint_eq(readOpenCount(file), 1);
|
||||
|
||||
/* An idle handle closes within twice the timeout */
|
||||
advanceClock(201);
|
||||
ck_assert_uint_eq(readOpenCount(file), 0);
|
||||
callClose(file, h, UA_STATUSCODE_BADINVALIDARGUMENT);
|
||||
|
||||
/* Without the parameter, the handles stay open */
|
||||
driver->stop(driver);
|
||||
ck_assert_uint_eq(UA_KeyValueMap_remove(&driver->params,
|
||||
UA_QUALIFIEDNAME(0, "activity-timeout")), UA_STATUSCODE_GOOD);
|
||||
ck_assert_uint_eq(driver->start(driver), UA_STATUSCODE_GOOD);
|
||||
h = callOpen(file, UA_OPENFILEMODE_READ, UA_STATUSCODE_GOOD);
|
||||
advanceClock(10 * 60 * 1000);
|
||||
ck_assert_uint_eq(readOpenCount(file), 1);
|
||||
callClose(file, h, UA_STATUSCODE_GOOD);
|
||||
ck_assert_uint_eq(UA_Server_run_shutdown(server_ft), UA_STATUSCODE_GOOD);
|
||||
UA_NodeId_clear(&file);
|
||||
} END_TEST
|
||||
|
||||
/* Closing an idle handle also removes a vanished file it kept */
|
||||
START_TEST(activityTimeoutClosesZombies) {
|
||||
UA_Server_getConfig(server_ft)->tcpEnabled = false;
|
||||
ck_assert_uint_eq(UA_Server_run_startup(server_ft), UA_STATUSCODE_GOOD);
|
||||
UA_FileTransferBackend backend = memBackendWithTree();
|
||||
UA_NodeId root;
|
||||
UA_Driver *driver = newTestDirectory(server_ft, &backend, &root);
|
||||
UA_Double interval = 0;
|
||||
ck_assert_uint_eq(UA_KeyValueMap_setScalar(&driver->params,
|
||||
UA_QUALIFIEDNAME(0, "refresh-interval"), &interval,
|
||||
&UA_TYPES[UA_TYPES_DOUBLE]), UA_STATUSCODE_GOOD);
|
||||
setActivityTimeout(driver, 100);
|
||||
ck_assert_uint_eq(UA_Server_addDriver(server_ft, driver), UA_STATUSCODE_GOOD);
|
||||
UA_NodeId docs = resolveChild(server_ft, root, "docs");
|
||||
UA_NodeId file = resolveChild(server_ft, docs, "a.txt");
|
||||
callOpen(file, UA_OPENFILEMODE_READ, UA_STATUSCODE_GOOD);
|
||||
ck_assert_uint_eq(backend.remove(&backend, UA_STRING("docs/a.txt")),
|
||||
UA_STATUSCODE_GOOD);
|
||||
ck_assert_uint_eq(testRefresh(driver, root), UA_STATUSCODE_GOOD);
|
||||
ck_assert(tryResolveChild(server_ft, docs, "a.txt", NULL));
|
||||
advanceClock(201);
|
||||
ck_assert(!tryResolveChild(server_ft, docs, "a.txt", NULL));
|
||||
ck_assert_uint_eq(UA_Server_run_shutdown(server_ft), UA_STATUSCODE_GOOD);
|
||||
UA_NodeId_clear(&file);
|
||||
UA_NodeId_clear(&docs);
|
||||
UA_NodeId_clear(&root);
|
||||
} END_TEST
|
||||
|
||||
START_TEST(activityTimeoutParameter) {
|
||||
UA_FileTransferBackend backend = memBackendWithFile("f.bin", "content");
|
||||
UA_Driver *driver = newTestFile(server_ft, &backend.file, "Timed", NULL);
|
||||
ck_assert_uint_eq(UA_Server_addDriver(server_ft, driver), UA_STATUSCODE_GOOD);
|
||||
/* Negative, NaN, infinite and sub-tick timeouts are rejected */
|
||||
UA_Double zero = 0.0;
|
||||
UA_Double invalid[4] = {-1, zero / zero, 1.0 / zero, 0.00001};
|
||||
for(size_t i = 0; i < 4; i++) {
|
||||
setActivityTimeout(driver, invalid[i]);
|
||||
ck_assert_uint_eq(driver->start(driver), UA_STATUSCODE_BADINVALIDARGUMENT);
|
||||
}
|
||||
UA_UInt32 wrong = 100;
|
||||
ck_assert_uint_eq(UA_KeyValueMap_setScalar(&driver->params,
|
||||
UA_QUALIFIEDNAME(0, "activity-timeout"), &wrong,
|
||||
&UA_TYPES[UA_TYPES_UINT32]), UA_STATUSCODE_GOOD);
|
||||
ck_assert_uint_eq(driver->start(driver), UA_STATUSCODE_BADINVALIDARGUMENT);
|
||||
setActivityTimeout(driver, 0);
|
||||
ck_assert_uint_eq(driver->start(driver), UA_STATUSCODE_GOOD);
|
||||
} END_TEST
|
||||
|
||||
/* A negative refresh interval is rejected. 0 disables the periodic refresh;
|
||||
* the application reconciles on demand. */
|
||||
/* A known parameter with the wrong type fails the start instead of being
|
||||
@@ -6366,6 +6471,9 @@ int main(void) {
|
||||
tcase_add_test(tc_lifecycle, failedRegistrationRetainsObjects);
|
||||
tcase_add_test(tc_lifecycle, directoryLifetimeAndAutomaticRefresh);
|
||||
tcase_add_test(tc_lifecycle, disabledPeriodicRefresh);
|
||||
tcase_add_test(tc_lifecycle, activityTimeoutClosesIdleHandles);
|
||||
tcase_add_test(tc_lifecycle, activityTimeoutClosesZombies);
|
||||
tcase_add_test(tc_lifecycle, activityTimeoutParameter);
|
||||
tcase_add_test(tc_lifecycle, driverParametersRequireTheirTypes);
|
||||
tcase_add_test(tc_lifecycle, directoryBackendCastsFileBackend);
|
||||
tcase_add_test(tc_lifecycle, defaultNodeDescriptions);
|
||||
|
||||
Reference in New Issue
Block a user