Commit 059e1bdf authored by Dag Erling Smørgrav's avatar Dag Erling Smørgrav

Mostly finish the LRU code and its integration:

 - Wrap the storage code so we don't need to duplicate the "toss out some old
   crap and try again" logic everywhere.  This will also help when / if we
   decide to add support for multiple concurrent storage arenas.

 - While I'm at it, implement sma_trim().

 - Rework the interaction between the LRU and expiry code.  Instead of placing
   objects retired by the LRU on death row, immediately terminate them.

 - Give the LRU code its own fake session and worker so we don't have to pass
   it a session pointer.

 - Rework the LRU API, and add LRU_DiscardOne() which discards a single
   object.  This is what the stevedore code uses.

Known or suspected issues:

 - The LRU and expiry code should use the same mutex, and / or the possiblity
   for races between them should be examined closely.

 - LRU_Init() needs to be looked at and possibly moved.

 - LRU_DiscardSpace() and LRU_DiscardTime() are unused and quite possibly useless.

 - Logging and statistics related to the LRU need more attention.

 - The stevedore API can probably be improved.


git-svn-id: http://www.varnish-cache.org/svn/trunk/varnish-cache@1586 d4fa192b-c00b-0410-8231-f00ffab90ce4
parent c8e9f809
...@@ -42,6 +42,7 @@ varnishd_SOURCES = \ ...@@ -42,6 +42,7 @@ varnishd_SOURCES = \
mgt_vcc.c \ mgt_vcc.c \
rfc2616.c \ rfc2616.c \
shmlog.c \ shmlog.c \
stevedore.c \
storage_file.c \ storage_file.c \
storage_malloc.c \ storage_malloc.c \
tcp.c \ tcp.c \
......
...@@ -375,7 +375,7 @@ void CLI_Init(void); ...@@ -375,7 +375,7 @@ void CLI_Init(void);
void EXP_Insert(struct object *o); void EXP_Insert(struct object *o);
void EXP_Init(void); void EXP_Init(void);
void EXP_TTLchange(struct object *o); void EXP_TTLchange(struct object *o);
void EXP_Retire(struct object *o); void EXP_Terminate(struct object *o);
/* cache_fetch.c */ /* cache_fetch.c */
int Fetch(struct sess *sp); int Fetch(struct sess *sp);
...@@ -478,10 +478,12 @@ void VCL_Rel(struct VCL_conf **vcc); ...@@ -478,10 +478,12 @@ void VCL_Rel(struct VCL_conf **vcc);
void VCL_Get(struct VCL_conf **vcc); void VCL_Get(struct VCL_conf **vcc);
/* cache_lru.c */ /* cache_lru.c */
// void LRU_Init(void);
void LRU_Enter(struct object *o, time_t stamp); void LRU_Enter(struct object *o, time_t stamp);
void LRU_Remove(struct object *o); void LRU_Remove(struct object *o);
void LRU_DiscardSpace(struct sess *sp, uint64_t quota); int LRU_DiscardOne(void);
void LRU_DiscardTime(struct sess *sp, time_t cutoff); int LRU_DiscardSpace(int64_t quota);
int LRU_DiscardTime(time_t cutoff);
#define VCL_RET_MAC(l,u,b,n) #define VCL_RET_MAC(l,u,b,n)
#define VCL_MET_MAC(l,u,b) void VCL_##l##_method(struct sess *); #define VCL_MET_MAC(l,u,b) void VCL_##l##_method(struct sess *);
......
...@@ -74,13 +74,25 @@ EXP_TTLchange(struct object *o) ...@@ -74,13 +74,25 @@ EXP_TTLchange(struct object *o)
UNLOCK(&exp_mtx); UNLOCK(&exp_mtx);
} }
/*
* Immediately destroy an object. Do not wait for it to expire or trickle
* through death row; yank it
*/
void void
EXP_Retire(struct object *o) EXP_Terminate(struct object *o)
{ {
LOCK(&exp_mtx); LOCK(&exp_mtx);
TAILQ_INSERT_TAIL(&exp_deathrow, o, deathrow); if (o->lru_stamp)
VSL_stats->n_deathrow++; LRU_Remove(o);
if (o->heap_idx)
binheap_delete(exp_heap, o->heap_idx);
if (o->deathrow.tqe_next) {
TAILQ_REMOVE(&exp_deathrow, o, deathrow);
VSL_stats->n_deathrow--;
}
UNLOCK(&exp_mtx); UNLOCK(&exp_mtx);
VSL(SLT_Terminate, 0, "%u", o->xid);
HSH_Deref(o);
} }
/*-------------------------------------------------------------------- /*--------------------------------------------------------------------
...@@ -183,7 +195,10 @@ exp_prefetch(void *arg) ...@@ -183,7 +195,10 @@ exp_prefetch(void *arg)
VCL_timeout_method(sp); VCL_timeout_method(sp);
if (sp->handling == VCL_RET_DISCARD) { if (sp->handling == VCL_RET_DISCARD) {
EXP_Retire(o); LOCK(&exp_mtx);
TAILQ_INSERT_TAIL(&exp_deathrow, o, deathrow);
VSL_stats->n_deathrow++;
UNLOCK(&exp_mtx);
continue; continue;
} }
assert(sp->handling == VCL_RET_DISCARD); assert(sp->handling == VCL_RET_DISCARD);
......
...@@ -59,8 +59,7 @@ fetch_straight(const struct sess *sp, int fd, struct http *hp, char *b) ...@@ -59,8 +59,7 @@ fetch_straight(const struct sess *sp, int fd, struct http *hp, char *b)
if (cl == 0) if (cl == 0)
return (0); return (0);
st = stevedore->alloc(stevedore, cl); st = STV_alloc(cl);
XXXAN(st->stevedore);
TAILQ_INSERT_TAIL(&sp->obj->store, st, list); TAILQ_INSERT_TAIL(&sp->obj->store, st, list);
st->len = cl; st->len = cl;
sp->obj->len = cl; sp->obj->len = cl;
...@@ -147,11 +146,9 @@ fetch_chunked(const struct sess *sp, int fd, struct http *hp) ...@@ -147,11 +146,9 @@ fetch_chunked(const struct sess *sp, int fd, struct http *hp)
/* Get some storage if we don't have any */ /* Get some storage if we don't have any */
if (st == NULL || st->len == st->space) { if (st == NULL || st->len == st->space) {
v = u; v = u;
if (u < params->fetch_chunksize * 1024 && if (u < params->fetch_chunksize * 1024)
stevedore->trim != NULL)
v = params->fetch_chunksize * 1024; v = params->fetch_chunksize * 1024;
st = stevedore->alloc(stevedore, v); st = STV_alloc(v);
XXXAN(st->stevedore);
TAILQ_INSERT_TAIL(&sp->obj->store, st, list); TAILQ_INSERT_TAIL(&sp->obj->store, st, list);
} }
v = st->space - st->len; v = st->space - st->len;
...@@ -198,9 +195,9 @@ fetch_chunked(const struct sess *sp, int fd, struct http *hp) ...@@ -198,9 +195,9 @@ fetch_chunked(const struct sess *sp, int fd, struct http *hp)
if (st != NULL && st->len == 0) { if (st != NULL && st->len == 0) {
TAILQ_REMOVE(&sp->obj->store, st, list); TAILQ_REMOVE(&sp->obj->store, st, list);
stevedore->free(st); STV_free(st);
} else if (st != NULL && stevedore->trim != NULL) } else if (st != NULL)
stevedore->trim(st, st->len); STV_trim(st, st->len);
return (0); return (0);
} }
...@@ -226,9 +223,7 @@ fetch_eof(const struct sess *sp, int fd, struct http *hp) ...@@ -226,9 +223,7 @@ fetch_eof(const struct sess *sp, int fd, struct http *hp)
st = NULL; st = NULL;
while (1) { while (1) {
if (v == 0) { if (v == 0) {
st = stevedore->alloc(stevedore, st = STV_alloc(params->fetch_chunksize * 1024);
params->fetch_chunksize * 1024);
XXXAN(st->stevedore);
TAILQ_INSERT_TAIL(&sp->obj->store, st, list); TAILQ_INSERT_TAIL(&sp->obj->store, st, list);
p = st->ptr + st->len; p = st->ptr + st->len;
v = st->space - st->len; v = st->space - st->len;
...@@ -248,9 +243,9 @@ fetch_eof(const struct sess *sp, int fd, struct http *hp) ...@@ -248,9 +243,9 @@ fetch_eof(const struct sess *sp, int fd, struct http *hp)
if (st->len == 0) { if (st->len == 0) {
TAILQ_REMOVE(&sp->obj->store, st, list); TAILQ_REMOVE(&sp->obj->store, st, list);
stevedore->free(st); STV_free(st);
} else if (stevedore->trim != NULL) } else
stevedore->trim(st, st->len); STV_trim(st, st->len);
return (1); return (1);
} }
...@@ -345,7 +340,7 @@ Fetch(struct sess *sp) ...@@ -345,7 +340,7 @@ Fetch(struct sess *sp)
while (!TAILQ_EMPTY(&sp->obj->store)) { while (!TAILQ_EMPTY(&sp->obj->store)) {
st = TAILQ_FIRST(&sp->obj->store); st = TAILQ_FIRST(&sp->obj->store);
TAILQ_REMOVE(&sp->obj->store, st, list); TAILQ_REMOVE(&sp->obj->store, st, list);
stevedore->free(st); STV_free(st);
} }
close(vc->fd); close(vc->fd);
VBE_ClosedFd(sp->wrk, vc, 1); VBE_ClosedFd(sp->wrk, vc, 1);
......
...@@ -104,7 +104,7 @@ HSH_Freestore(struct object *o) ...@@ -104,7 +104,7 @@ HSH_Freestore(struct object *o)
TAILQ_FOREACH_SAFE(st, &o->store, list, stn) { TAILQ_FOREACH_SAFE(st, &o->store, list, stn) {
CHECK_OBJ_NOTNULL(st, STORAGE_MAGIC); CHECK_OBJ_NOTNULL(st, STORAGE_MAGIC);
TAILQ_REMOVE(&o->store, st, list); TAILQ_REMOVE(&o->store, st, list);
st->stevedore->free(st); STV_free(st);
} }
} }
...@@ -260,7 +260,6 @@ HSH_Deref(struct object *o) ...@@ -260,7 +260,6 @@ HSH_Deref(struct object *o)
free(o->vary); free(o->vary);
HSH_Freestore(o); HSH_Freestore(o);
LRU_Remove(o);
FREE_OBJ(o); FREE_OBJ(o);
VSL_stats->n_object--; VSL_stats->n_object--;
......
...@@ -40,8 +40,31 @@ ...@@ -40,8 +40,31 @@
*/ */
#define LRU_DELAY 2 #define LRU_DELAY 2
TAILQ_HEAD(lru_head, object);
static struct lru_head lru_list = TAILQ_HEAD_INITIALIZER(lru_list);
static pthread_mutex_t lru_mtx = PTHREAD_MUTEX_INITIALIZER; static pthread_mutex_t lru_mtx = PTHREAD_MUTEX_INITIALIZER;
static TAILQ_HEAD(lru_head, object) lru_list; static struct sess *lru_session;
static struct worker lru_worker;
/*
* Initialize the LRU data structures.
*/
static inline void
LRU_Init(void)
{
if (lru_session == NULL) {
lru_session = SES_New(NULL, 0);
XXXAN(lru_session);
lru_session->wrk = &lru_worker;
lru_worker.magic = WORKER_MAGIC;
lru_worker.wlp = lru_worker.wlog;
lru_worker.wle = lru_worker.wlog + sizeof lru_worker.wlog;
VCL_Get(&lru_session->vcl);
} else {
VCL_Refresh(&lru_session->vcl);
}
}
/* /*
* Enter an object into the LRU list, or move it to the head of the list * Enter an object into the LRU list, or move it to the head of the list
...@@ -55,12 +78,12 @@ LRU_Enter(struct object *o, time_t stamp) ...@@ -55,12 +78,12 @@ LRU_Enter(struct object *o, time_t stamp)
assert(stamp > 0); assert(stamp > 0);
if (o->lru_stamp < stamp - LRU_DELAY && o != lru_list.tqh_first) { if (o->lru_stamp < stamp - LRU_DELAY && o != lru_list.tqh_first) {
// VSL(SLT_LRU_enter, 0, "%u %u %u", o->xid, o->lru_stamp, stamp); // VSL(SLT_LRU_enter, 0, "%u %u %u", o->xid, o->lru_stamp, stamp);
pthread_mutex_lock(&lru_mtx); LOCK(&lru_mtx);
if (o->lru_stamp != 0) if (o->lru_stamp != 0)
TAILQ_REMOVE(&lru_list, o, lru); TAILQ_REMOVE(&lru_list, o, lru);
TAILQ_INSERT_HEAD(&lru_list, o, lru); TAILQ_INSERT_HEAD(&lru_list, o, lru);
o->lru_stamp = stamp; o->lru_stamp = stamp;
pthread_mutex_unlock(&lru_mtx); UNLOCK(&lru_mtx);
} }
} }
...@@ -74,74 +97,123 @@ LRU_Remove(struct object *o) ...@@ -74,74 +97,123 @@ LRU_Remove(struct object *o)
CHECK_OBJ_NOTNULL(o, OBJECT_MAGIC); CHECK_OBJ_NOTNULL(o, OBJECT_MAGIC);
if (o->lru_stamp != 0) { if (o->lru_stamp != 0) {
// VSL(SLT_LRU_remove, 0, "%u", o->xid); // VSL(SLT_LRU_remove, 0, "%u", o->xid);
pthread_mutex_lock(&lru_mtx); LOCK(&lru_mtx);
TAILQ_REMOVE(&lru_list, o, lru); TAILQ_REMOVE(&lru_list, o, lru);
pthread_mutex_unlock(&lru_mtx); UNLOCK(&lru_mtx);
} }
} }
/* /*
* Walk through the LRU list, starting at the back, and retire objects * With the LRU lock held, call VCL_discard(). Depending on the result,
* until our quota is reached or we run out of objects to retire. * either insert the object at the head of the list or dereference it.
*/ */
void static int
LRU_DiscardSpace(struct sess *sp, uint64_t quota) LRU_DiscardLocked(struct object *o)
{ {
struct object *o, *so; struct object *so;
if (o->busy)
return (0);
pthread_mutex_lock(&lru_mtx); /* XXX this is a really bad place to do this */
while ((o = TAILQ_LAST(&lru_list, lru_head))) { LRU_Init();
CHECK_OBJ_NOTNULL(o, OBJECT_MAGIC);
TAILQ_REMOVE(&lru_list, o, lru); TAILQ_REMOVE(&lru_list, o, lru);
so = sp->obj;
sp->obj = o; lru_session->obj = o;
VCL_discard_method(sp); VCL_discard_method(lru_session);
sp->obj = so;
if (sp->handling == VCL_RET_DISCARD) { if (lru_session->handling == VCL_RET_DISCARD) {
/* discard: place on deathrow */ /* discard: release object */
EXP_Retire(o); VSL(SLT_ExpKill, 0, "%u %d", o->xid, o->lru_stamp);
o->lru_stamp = 0; o->lru_stamp = 0;
if (o->len > quota) EXP_Terminate(o);
break; return (1);
quota -= o->len;
} else { } else {
/* keep: move to front of list */ /* keep: move to front of list */
if ((so = TAILQ_FIRST(&lru_list))) if ((so = TAILQ_FIRST(&lru_list)))
o->lru_stamp = so->lru_stamp; o->lru_stamp = so->lru_stamp;
TAILQ_INSERT_HEAD(&lru_list, o, lru); TAILQ_INSERT_HEAD(&lru_list, o, lru);
return (0);
}
}
/*
* Walk through the LRU list, starting at the back, and check each object
* until we find one that can be retired. Return the number of objects
* that were discarded.
*/
int
LRU_DiscardOne(void)
{
struct object *first = TAILQ_FIRST(&lru_list);
struct object *o;
int count = 0;
LOCK(&lru_mtx);
while (!count && (o = TAILQ_LAST(&lru_list, lru_head))) {
if (LRU_DiscardLocked(o))
++count;
if (o == first) {
/* full circle */
break;
} }
} }
pthread_mutex_unlock(&lru_mtx); UNLOCK(&lru_mtx);
return (0);
} }
/* /*
* Walk through the LRU list, starting at the back, and retire objects * Walk through the LRU list, starting at the back, and retire objects
* that haven't been accessed since the specified cutoff date. * until our quota is reached or we run out of objects to retire. Return
* the number of objects that were discarded.
*/ */
void int
LRU_DiscardTime(struct sess *sp, time_t cutoff) LRU_DiscardSpace(int64_t quota)
{ {
struct object *o, *so; struct object *first = TAILQ_FIRST(&lru_list);
struct object *o;
unsigned int len;
int count = 0;
pthread_mutex_lock(&lru_mtx); LOCK(&lru_mtx);
while ((o = TAILQ_LAST(&lru_list, lru_head))) { while (quota > 0 && (o = TAILQ_LAST(&lru_list, lru_head))) {
if (o->lru_stamp >= cutoff) len = o->len;
if (LRU_DiscardLocked(o)) {
quota -= len;
++count;
}
if (o == first) {
/* full circle */
break;
}
}
UNLOCK(&lru_mtx);
return (count);
}
/*
* Walk through the LRU list, starting at the back, and retire objects
* that haven't been accessed since the specified cutoff date. Return the
* number of objects that were discarded.
*/
int
LRU_DiscardTime(time_t cutoff)
{
struct object *first = TAILQ_FIRST(&lru_list);
struct object *o;
int count = 0;
LOCK(&lru_mtx);
while ((o = TAILQ_LAST(&lru_list, lru_head)) && o->lru_stamp <= cutoff) {
if (LRU_DiscardLocked(o))
++count;
if (o == first) {
/* full circle */
break; break;
TAILQ_REMOVE(&lru_list, o, lru);
so = sp->obj;
sp->obj = o;
VCL_discard_method(sp);
sp->obj = so;
if (sp->handling == VCL_RET_DISCARD) {
/* discard: place on deathrow */
EXP_Retire(o);
} else {
/* keep: move to front of list */
if ((so = TAILQ_FIRST(&lru_list)) && so->lru_stamp > cutoff)
o->lru_stamp = so->lru_stamp;
else
o->lru_stamp = cutoff;
TAILQ_INSERT_HEAD(&lru_list, o, lru);
} }
} }
pthread_mutex_unlock(&lru_mtx); UNLOCK(&lru_mtx);
return (count);
} }
...@@ -90,7 +90,7 @@ SYN_ErrorPage(struct sess *sp, int status, const char *reason, int ttl) ...@@ -90,7 +90,7 @@ SYN_ErrorPage(struct sess *sp, int status, const char *reason, int ttl)
/* allocate space for body */ /* allocate space for body */
/* XXX what if the object already has a body? */ /* XXX what if the object already has a body? */
st = stevedore->alloc(stevedore, 1024); st = STV_alloc(1024);
XXXAN(st->stevedore); XXXAN(st->stevedore);
TAILQ_INSERT_TAIL(&sp->obj->store, st, list); TAILQ_INSERT_TAIL(&sp->obj->store, st, list);
......
/*-
* Copyright (c) 2007 Linpro AS
* All rights reserved.
*
* Author: Dag-Erling Smørgav <des@linpro.no>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* $Id$
*/
#include "cache.h"
struct storage *
STV_alloc(size_t size)
{
struct storage *st;
AN(stevedore);
AN(stevedore->alloc);
do {
if ((st = stevedore->alloc(stevedore, size)) == NULL)
LRU_DiscardOne();
} while (st == NULL);
return (st);
}
void
STV_trim(struct storage *st, size_t size)
{
AN(st->stevedore);
if (st->stevedore->trim)
st->stevedore->trim(st, size);
}
void
STV_free(struct storage *st)
{
AN(st->stevedore);
AN(stevedore->free);
st->stevedore->free(st);
}
...@@ -50,3 +50,7 @@ struct stevedore { ...@@ -50,3 +50,7 @@ struct stevedore {
/* private fields */ /* private fields */
void *priv; void *priv;
}; };
struct storage *STV_alloc(size_t size);
void STV_trim(struct storage *st, size_t size);
void STV_free(struct storage *st);
...@@ -631,6 +631,10 @@ smf_alloc(struct stevedore *st, size_t size) ...@@ -631,6 +631,10 @@ smf_alloc(struct stevedore *st, size_t size)
LOCK(&sc->mtx); LOCK(&sc->mtx);
VSL_stats->sm_nreq++; VSL_stats->sm_nreq++;
smf = alloc_smf(sc, size); smf = alloc_smf(sc, size);
if (smf == NULL) {
UNLOCK(&sc->mtx);
return (NULL);
}
CHECK_OBJ_NOTNULL(smf, SMF_MAGIC); CHECK_OBJ_NOTNULL(smf, SMF_MAGIC);
VSL_stats->sm_nobj++; VSL_stats->sm_nobj++;
VSL_stats->sm_balloc += smf->size; VSL_stats->sm_balloc += smf->size;
......
...@@ -49,7 +49,8 @@ sma_alloc(struct stevedore *st, size_t size) ...@@ -49,7 +49,8 @@ sma_alloc(struct stevedore *st, size_t size)
VSL_stats->sm_nreq++; VSL_stats->sm_nreq++;
sma = calloc(sizeof *sma, 1); sma = calloc(sizeof *sma, 1);
XXXAN(sma); if (sma == NULL)
return (NULL);
sma->s.priv = sma; sma->s.priv = sma;
sma->s.ptr = malloc(size); sma->s.ptr = malloc(size);
XXXAN(sma->s.ptr); XXXAN(sma->s.ptr);
...@@ -68,6 +69,7 @@ sma_free(struct storage *s) ...@@ -68,6 +69,7 @@ sma_free(struct storage *s)
{ {
struct sma *sma; struct sma *sma;
CHECK_OBJ_NOTNULL(s, STORAGE_MAGIC);
sma = s->priv; sma = s->priv;
VSL_stats->sm_nobj--; VSL_stats->sm_nobj--;
VSL_stats->sm_balloc -= sma->s.space; VSL_stats->sm_balloc -= sma->s.space;
...@@ -75,8 +77,25 @@ sma_free(struct storage *s) ...@@ -75,8 +77,25 @@ sma_free(struct storage *s)
free(sma); free(sma);
} }
static void
sma_trim(struct storage *s, size_t size)
{
struct sma *sma;
void *p;
CHECK_OBJ_NOTNULL(s, STORAGE_MAGIC);
sma = s->priv;
if ((p = realloc(sma->s.ptr, size)) != NULL) {
VSL_stats->sm_balloc -= sma->s.space;
sma->s.ptr = p;
sma->s.space = size;
VSL_stats->sm_balloc += sma->s.space;
}
}
struct stevedore sma_stevedore = { struct stevedore sma_stevedore = {
.name = "malloc", .name = "malloc",
.alloc = sma_alloc, .alloc = sma_alloc,
.free = sma_free .free = sma_free,
.trim = sma_trim,
}; };
...@@ -92,3 +92,4 @@ SLTM(ExpBan) ...@@ -92,3 +92,4 @@ SLTM(ExpBan)
SLTM(ExpPick) SLTM(ExpPick)
SLTM(ExpKill) SLTM(ExpKill)
SLTM(WorkThread) SLTM(WorkThread)
SLTM(Terminate)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment