1
0
mirror of https://github.com/moparisthebest/curl synced 2024-12-21 23:58:49 -05:00

multi: use timeouts properly for MAX_RECV/SEND_SPEED

When detecting that the send or recv speed, the multi interface changes
state to TOOFAST and previously there was no timeout set that would
force a recheck but it would rely on the application to somehow call
libcurl anyway. This now sets a timeout for a suitable future time to
check again if the average transfer speed is then below the threshold
again.
This commit is contained in:
Daniel Stenberg 2010-08-10 14:12:05 +02:00
parent 232ad6549a
commit 9124bfba45

View File

@ -934,10 +934,13 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
bool done = FALSE; bool done = FALSE;
CURLMcode result = CURLM_OK; CURLMcode result = CURLM_OK;
struct SingleRequest *k; struct SingleRequest *k;
struct SessionHandle *data;
if(!GOOD_EASY_HANDLE(easy->easy_handle)) if(!GOOD_EASY_HANDLE(easy->easy_handle))
return CURLM_BAD_EASY_HANDLE; return CURLM_BAD_EASY_HANDLE;
data = easy->easy_handle;
do { do {
/* this is a do-while loop just to allow a break to skip to the end /* this is a do-while loop just to allow a break to skip to the end
of it */ of it */
@ -945,9 +948,9 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
/* Handle the case when the pipe breaks, i.e., the connection /* Handle the case when the pipe breaks, i.e., the connection
we're using gets cleaned up and we're left with nothing. */ we're using gets cleaned up and we're left with nothing. */
if(easy->easy_handle->state.pipe_broke) { if(data->state.pipe_broke) {
infof(easy->easy_handle, "Pipe broke: handle 0x%p, url = %s\n", infof(data, "Pipe broke: handle 0x%p, url = %s\n",
easy, easy->easy_handle->state.path); easy, data->state.path);
if(easy->state != CURLM_STATE_COMPLETED) { if(easy->state != CURLM_STATE_COMPLETED) {
/* Head back to the CONNECT state */ /* Head back to the CONNECT state */
@ -956,7 +959,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
easy->result = CURLE_OK; easy->result = CURLE_OK;
} }
easy->easy_handle->state.pipe_broke = FALSE; data->state.pipe_broke = FALSE;
easy->easy_conn = NULL; easy->easy_conn = NULL;
break; break;
} }
@ -964,31 +967,31 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
if(easy->easy_conn && easy->state > CURLM_STATE_CONNECT && if(easy->easy_conn && easy->state > CURLM_STATE_CONNECT &&
easy->state < CURLM_STATE_COMPLETED) easy->state < CURLM_STATE_COMPLETED)
/* Make sure we set the connection's current owner */ /* Make sure we set the connection's current owner */
easy->easy_conn->data = easy->easy_handle; easy->easy_conn->data = data;
switch(easy->state) { switch(easy->state) {
case CURLM_STATE_INIT: case CURLM_STATE_INIT:
/* init this transfer. */ /* init this transfer. */
easy->result=Curl_pretransfer(easy->easy_handle); easy->result=Curl_pretransfer(data);
if(CURLE_OK == easy->result) { if(CURLE_OK == easy->result) {
/* after init, go CONNECT */ /* after init, go CONNECT */
multistate(easy, CURLM_STATE_CONNECT); multistate(easy, CURLM_STATE_CONNECT);
result = CURLM_CALL_MULTI_PERFORM; result = CURLM_CALL_MULTI_PERFORM;
easy->easy_handle->state.used_interface = Curl_if_multi; data->state.used_interface = Curl_if_multi;
} }
break; break;
case CURLM_STATE_CONNECT: case CURLM_STATE_CONNECT:
/* Connect. We get a connection identifier filled in. */ /* Connect. We get a connection identifier filled in. */
Curl_pgrsTime(easy->easy_handle, TIMER_STARTSINGLE); Curl_pgrsTime(data, TIMER_STARTSINGLE);
easy->result = Curl_connect(easy->easy_handle, &easy->easy_conn, easy->result = Curl_connect(data, &easy->easy_conn,
&async, &protocol_connect); &async, &protocol_connect);
if(CURLE_OK == easy->result) { if(CURLE_OK == easy->result) {
/* Add this handle to the send or pend pipeline */ /* Add this handle to the send or pend pipeline */
easy->result = addHandleToSendOrPendPipeline(easy->easy_handle, easy->result = addHandleToSendOrPendPipeline(data,
easy->easy_conn); easy->easy_conn);
if(CURLE_OK == easy->result) { if(CURLE_OK == easy->result) {
if(async) if(async)
@ -1071,9 +1074,9 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
if(easy->easy_conn->bits.proxy_connect_closed) { if(easy->easy_conn->bits.proxy_connect_closed) {
/* reset the error buffer */ /* reset the error buffer */
if(easy->easy_handle->set.errorbuffer) if(data->set.errorbuffer)
easy->easy_handle->set.errorbuffer[0] = '\0'; data->set.errorbuffer[0] = '\0';
easy->easy_handle->state.errorbuf = FALSE; data->state.errorbuf = FALSE;
easy->result = CURLE_OK; easy->result = CURLE_OK;
result = CURLM_CALL_MULTI_PERFORM; result = CURLM_CALL_MULTI_PERFORM;
@ -1145,7 +1148,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
} }
else if(easy->result) { else if(easy->result) {
/* failure detected */ /* failure detected */
Curl_posttransfer(easy->easy_handle); Curl_posttransfer(data);
Curl_done(&easy->easy_conn, easy->result, FALSE); Curl_done(&easy->easy_conn, easy->result, FALSE);
disconnect_conn = TRUE; disconnect_conn = TRUE;
} }
@ -1154,15 +1157,15 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
case CURLM_STATE_WAITDO: case CURLM_STATE_WAITDO:
/* Wait for our turn to DO when we're pipelining requests */ /* Wait for our turn to DO when we're pipelining requests */
#ifdef DEBUGBUILD #ifdef DEBUGBUILD
infof(easy->easy_handle, "Conn %ld send pipe %zu inuse %d athead %d\n", infof(data, "Conn %ld send pipe %zu inuse %d athead %d\n",
easy->easy_conn->connectindex, easy->easy_conn->connectindex,
easy->easy_conn->send_pipe->size, easy->easy_conn->send_pipe->size,
easy->easy_conn->writechannel_inuse?1:0, easy->easy_conn->writechannel_inuse?1:0,
isHandleAtHead(easy->easy_handle, isHandleAtHead(data,
easy->easy_conn->send_pipe)?1:0); easy->easy_conn->send_pipe)?1:0);
#endif #endif
if(!easy->easy_conn->writechannel_inuse && if(!easy->easy_conn->writechannel_inuse &&
isHandleAtHead(easy->easy_handle, isHandleAtHead(data,
easy->easy_conn->send_pipe)) { easy->easy_conn->send_pipe)) {
/* Grab the channel */ /* Grab the channel */
easy->easy_conn->writechannel_inuse = TRUE; easy->easy_conn->writechannel_inuse = TRUE;
@ -1172,7 +1175,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
break; break;
case CURLM_STATE_DO: case CURLM_STATE_DO:
if(easy->easy_handle->set.connect_only) { if(data->set.connect_only) {
/* keep connection open for application to use the socket */ /* keep connection open for application to use the socket */
easy->easy_conn->bits.close = FALSE; easy->easy_conn->bits.close = FALSE;
multistate(easy, CURLM_STATE_DONE); multistate(easy, CURLM_STATE_DONE);
@ -1187,8 +1190,8 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
if(CURLE_OK == easy->result) { if(CURLE_OK == easy->result) {
if(!dophase_done) { if(!dophase_done) {
/* some steps needed for wildcard matching */ /* some steps needed for wildcard matching */
if(easy->easy_handle->set.wildcardmatch) { if(data->set.wildcardmatch) {
struct WildcardData *wc = &easy->easy_handle->wildcard; struct WildcardData *wc = &data->wildcard;
if(wc->state == CURLWC_DONE || wc->state == CURLWC_SKIP) { if(wc->state == CURLWC_DONE || wc->state == CURLWC_SKIP) {
/* skip some states if it is important */ /* skip some states if it is important */
Curl_done(&easy->easy_conn, CURLE_OK, FALSE); Curl_done(&easy->easy_conn, CURLE_OK, FALSE);
@ -1237,7 +1240,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
else else
retry = (bool)(newurl?TRUE:FALSE); retry = (bool)(newurl?TRUE:FALSE);
Curl_posttransfer(easy->easy_handle); Curl_posttransfer(data);
drc = Curl_done(&easy->easy_conn, easy->result, FALSE); drc = Curl_done(&easy->easy_conn, easy->result, FALSE);
/* When set to retry the connection, we must to go back to /* When set to retry the connection, we must to go back to
@ -1245,7 +1248,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
if(retry) { if(retry) {
if ((drc == CURLE_OK) || (drc == CURLE_SEND_ERROR)) { if ((drc == CURLE_OK) || (drc == CURLE_SEND_ERROR)) {
follow = FOLLOW_RETRY; follow = FOLLOW_RETRY;
drc = Curl_follow(easy->easy_handle, newurl, follow); drc = Curl_follow(data, newurl, follow);
if(drc == CURLE_OK) { if(drc == CURLE_OK) {
multistate(easy, CURLM_STATE_CONNECT); multistate(easy, CURLM_STATE_CONNECT);
result = CURLM_CALL_MULTI_PERFORM; result = CURLM_CALL_MULTI_PERFORM;
@ -1270,7 +1273,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
} }
else { else {
/* failure detected */ /* failure detected */
Curl_posttransfer(easy->easy_handle); Curl_posttransfer(data);
Curl_done(&easy->easy_conn, easy->result, FALSE); Curl_done(&easy->easy_conn, easy->result, FALSE);
disconnect_conn = TRUE; disconnect_conn = TRUE;
} }
@ -1299,7 +1302,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
} }
else { else {
/* failure detected */ /* failure detected */
Curl_posttransfer(easy->easy_handle); Curl_posttransfer(data);
Curl_done(&easy->easy_conn, easy->result, FALSE); Curl_done(&easy->easy_conn, easy->result, FALSE);
disconnect_conn = TRUE; disconnect_conn = TRUE;
} }
@ -1325,7 +1328,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
} }
else { else {
/* failure detected */ /* failure detected */
Curl_posttransfer(easy->easy_handle); Curl_posttransfer(data);
Curl_done(&easy->easy_conn, easy->result, FALSE); Curl_done(&easy->easy_conn, easy->result, FALSE);
disconnect_conn = TRUE; disconnect_conn = TRUE;
} }
@ -1334,7 +1337,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
case CURLM_STATE_DO_DONE: case CURLM_STATE_DO_DONE:
/* Move ourselves from the send to recv pipeline */ /* Move ourselves from the send to recv pipeline */
moveHandleFromSendToRecvPipeline(easy->easy_handle, easy->easy_conn); moveHandleFromSendToRecvPipeline(data, easy->easy_conn);
/* Check if we can move pending requests to send pipe */ /* Check if we can move pending requests to send pipe */
checkPendPipeline(easy->easy_conn); checkPendPipeline(easy->easy_conn);
multistate(easy, CURLM_STATE_WAITPERFORM); multistate(easy, CURLM_STATE_WAITPERFORM);
@ -1344,7 +1347,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
case CURLM_STATE_WAITPERFORM: case CURLM_STATE_WAITPERFORM:
/* Wait for our turn to PERFORM */ /* Wait for our turn to PERFORM */
if(!easy->easy_conn->readchannel_inuse && if(!easy->easy_conn->readchannel_inuse &&
isHandleAtHead(easy->easy_handle, isHandleAtHead(data,
easy->easy_conn->recv_pipe)) { easy->easy_conn->recv_pipe)) {
/* Grab the channel */ /* Grab the channel */
easy->easy_conn->readchannel_inuse = TRUE; easy->easy_conn->readchannel_inuse = TRUE;
@ -1353,11 +1356,11 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
} }
#ifdef DEBUGBUILD #ifdef DEBUGBUILD
else { else {
infof(easy->easy_handle, "Conn %ld recv pipe %zu inuse %d athead %d\n", infof(data, "Conn %ld recv pipe %zu inuse %d athead %d\n",
easy->easy_conn->connectindex, easy->easy_conn->connectindex,
easy->easy_conn->recv_pipe->size, easy->easy_conn->recv_pipe->size,
easy->easy_conn->readchannel_inuse?1:0, easy->easy_conn->readchannel_inuse?1:0,
isHandleAtHead(easy->easy_handle, isHandleAtHead(data,
easy->easy_conn->recv_pipe)?1:0); easy->easy_conn->recv_pipe)?1:0);
} }
#endif #endif
@ -1365,36 +1368,52 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
case CURLM_STATE_TOOFAST: /* limit-rate exceeded in either direction */ case CURLM_STATE_TOOFAST: /* limit-rate exceeded in either direction */
/* if both rates are within spec, resume transfer */ /* if both rates are within spec, resume transfer */
if( ( ( easy->easy_handle->set.max_send_speed == 0 ) || if( ( (data->set.max_send_speed == 0) ||
( easy->easy_handle->progress.ulspeed < (data->progress.ulspeed < data->set.max_send_speed )) &&
easy->easy_handle->set.max_send_speed ) ) && ( (data->set.max_recv_speed == 0) ||
( ( easy->easy_handle->set.max_recv_speed == 0 ) || (data->progress.dlspeed < data->set.max_recv_speed) ) )
( easy->easy_handle->progress.dlspeed <
easy->easy_handle->set.max_recv_speed ) )
)
multistate(easy, CURLM_STATE_PERFORM); multistate(easy, CURLM_STATE_PERFORM);
break; break;
case CURLM_STATE_PERFORM: case CURLM_STATE_PERFORM:
/* check if over speed */ /* check if over send speed */
if( ( ( easy->easy_handle->set.max_send_speed > 0 ) && if( (data->set.max_send_speed > 0) &&
( easy->easy_handle->progress.ulspeed > (data->progress.ulspeed > data->set.max_send_speed) ) {
easy->easy_handle->set.max_send_speed ) ) || int buffersize;
( ( easy->easy_handle->set.max_recv_speed > 0 ) && long timeout_ms;
( easy->easy_handle->progress.dlspeed >
easy->easy_handle->set.max_recv_speed ) ) multistate(easy, CURLM_STATE_TOOFAST);
) {
/* Transfer is over the speed limit. Change state. TODO: Call /* calculate upload rate-limitation timeout. */
* Curl_expire() with the time left until we're targeted to be below buffersize = (int)(data->set.buffer_size ?
* the speed limit again. */ data->set.buffer_size : BUFSIZE);
multistate(easy, CURLM_STATE_TOOFAST ); timeout_ms = Curl_sleep_time(data->set.max_send_speed,
data->progress.ulspeed, buffersize);
Curl_expire(data, timeout_ms);
break;
}
/* check if over recv speed */
if( (data->set.max_recv_speed > 0) &&
(data->progress.dlspeed > data->set.max_recv_speed) ) {
int buffersize;
long timeout_ms;
multistate(easy, CURLM_STATE_TOOFAST);
/* Calculate download rate-limitation timeout. */
buffersize = (int)(data->set.buffer_size ?
data->set.buffer_size : BUFSIZE);
timeout_ms = Curl_sleep_time(data->set.max_recv_speed,
data->progress.dlspeed, buffersize);
Curl_expire(data, timeout_ms);
break; break;
} }
/* read/write data if it is ready to do so */ /* read/write data if it is ready to do so */
easy->result = Curl_readwrite(easy->easy_conn, &done); easy->result = Curl_readwrite(easy->easy_conn, &done);
k = &easy->easy_handle->req; k = &data->req;
if(!(k->keepon & KEEP_RECV)) { if(!(k->keepon & KEEP_RECV)) {
/* We're done receiving */ /* We're done receiving */
@ -1416,7 +1435,7 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
if(!(easy->easy_conn->protocol & PROT_DUALCHANNEL)) if(!(easy->easy_conn->protocol & PROT_DUALCHANNEL))
easy->easy_conn->bits.close = TRUE; easy->easy_conn->bits.close = TRUE;
Curl_posttransfer(easy->easy_handle); Curl_posttransfer(data);
Curl_done(&easy->easy_conn, easy->result, FALSE); Curl_done(&easy->easy_conn, easy->result, FALSE);
} }
else if(TRUE == done) { else if(TRUE == done) {
@ -1429,10 +1448,10 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
retry = (bool)(newurl?TRUE:FALSE); retry = (bool)(newurl?TRUE:FALSE);
/* call this even if the readwrite function returned error */ /* call this even if the readwrite function returned error */
Curl_posttransfer(easy->easy_handle); Curl_posttransfer(data);
/* we're no longer receving */ /* we're no longer receving */
moveHandleFromRecvToDonePipeline(easy->easy_handle, moveHandleFromRecvToDonePipeline(data,
easy->easy_conn); easy->easy_conn);
/* expire the new receiving pipeline head */ /* expire the new receiving pipeline head */
@ -1444,19 +1463,19 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
/* When we follow redirects or is set to retry the connection, we must /* When we follow redirects or is set to retry the connection, we must
to go back to the CONNECT state */ to go back to the CONNECT state */
if(easy->easy_handle->req.newurl || retry) { if(data->req.newurl || retry) {
if(!retry) { if(!retry) {
/* if the URL is a follow-location and not just a retried request /* if the URL is a follow-location and not just a retried request
then figure out the URL here */ then figure out the URL here */
newurl = easy->easy_handle->req.newurl; newurl = data->req.newurl;
easy->easy_handle->req.newurl = NULL; data->req.newurl = NULL;
follow = FOLLOW_REDIR; follow = FOLLOW_REDIR;
} }
else else
follow = FOLLOW_RETRY; follow = FOLLOW_RETRY;
easy->result = Curl_done(&easy->easy_conn, CURLE_OK, FALSE); easy->result = Curl_done(&easy->easy_conn, CURLE_OK, FALSE);
if(easy->result == CURLE_OK) if(easy->result == CURLE_OK)
easy->result = Curl_follow(easy->easy_handle, newurl, follow); easy->result = Curl_follow(data, newurl, follow);
if(CURLE_OK == easy->result) { if(CURLE_OK == easy->result) {
multistate(easy, CURLM_STATE_CONNECT); multistate(easy, CURLM_STATE_CONNECT);
result = CURLM_CALL_MULTI_PERFORM; result = CURLM_CALL_MULTI_PERFORM;
@ -1471,10 +1490,10 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
/* but first check to see if we got a location info even though we're /* but first check to see if we got a location info even though we're
not following redirects */ not following redirects */
if (easy->easy_handle->req.location) { if (data->req.location) {
newurl = easy->easy_handle->req.location; newurl = data->req.location;
easy->easy_handle->req.location = NULL; data->req.location = NULL;
easy->result = Curl_follow(easy->easy_handle, newurl, FOLLOW_FAKE); easy->result = Curl_follow(data, newurl, FOLLOW_FAKE);
if (easy->result) if (easy->result)
free(newurl); free(newurl);
} }
@ -1491,9 +1510,9 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
if(easy->easy_conn) { if(easy->easy_conn) {
/* Remove ourselves from the receive and done pipelines. Handle /* Remove ourselves from the receive and done pipelines. Handle
should be on one of these lists, depending upon how we got here. */ should be on one of these lists, depending upon how we got here. */
Curl_removeHandleFromPipeline(easy->easy_handle, Curl_removeHandleFromPipeline(data,
easy->easy_conn->recv_pipe); easy->easy_conn->recv_pipe);
Curl_removeHandleFromPipeline(easy->easy_handle, Curl_removeHandleFromPipeline(data,
easy->easy_conn->done_pipe); easy->easy_conn->done_pipe);
/* Check if we can move pending requests to send pipe */ /* Check if we can move pending requests to send pipe */
checkPendPipeline(easy->easy_conn); checkPendPipeline(easy->easy_conn);
@ -1517,8 +1536,8 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
easy->easy_conn = NULL; easy->easy_conn = NULL;
} }
if(easy->easy_handle->set.wildcardmatch) { if(data->set.wildcardmatch) {
if(easy->easy_handle->wildcard.state != CURLWC_DONE) { if(data->wildcard.state != CURLWC_DONE) {
/* if a wildcard is set and we are not ending -> lets start again /* if a wildcard is set and we are not ending -> lets start again
with CURLM_STATE_INIT */ with CURLM_STATE_INIT */
result = CURLM_CALL_MULTI_PERFORM; result = CURLM_CALL_MULTI_PERFORM;
@ -1558,17 +1577,17 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
/* NOTE: no attempt to disconnect connections must be made /* NOTE: no attempt to disconnect connections must be made
in the case blocks above - cleanup happens only here */ in the case blocks above - cleanup happens only here */
easy->easy_handle->state.pipe_broke = FALSE; data->state.pipe_broke = FALSE;
if(easy->easy_conn) { if(easy->easy_conn) {
/* if this has a connection, unsubscribe from the pipelines */ /* if this has a connection, unsubscribe from the pipelines */
easy->easy_conn->writechannel_inuse = FALSE; easy->easy_conn->writechannel_inuse = FALSE;
easy->easy_conn->readchannel_inuse = FALSE; easy->easy_conn->readchannel_inuse = FALSE;
Curl_removeHandleFromPipeline(easy->easy_handle, Curl_removeHandleFromPipeline(data,
easy->easy_conn->send_pipe); easy->easy_conn->send_pipe);
Curl_removeHandleFromPipeline(easy->easy_handle, Curl_removeHandleFromPipeline(data,
easy->easy_conn->recv_pipe); easy->easy_conn->recv_pipe);
Curl_removeHandleFromPipeline(easy->easy_handle, Curl_removeHandleFromPipeline(data,
easy->easy_conn->done_pipe); easy->easy_conn->done_pipe);
/* Check if we can move pending requests to send pipe */ /* Check if we can move pending requests to send pipe */
checkPendPipeline(easy->easy_conn); checkPendPipeline(easy->easy_conn);
@ -1592,17 +1611,17 @@ static CURLMcode multi_runsingle(struct Curl_multi *multi,
} while(0); } while(0);
if(CURLM_STATE_COMPLETED == easy->state) { if(CURLM_STATE_COMPLETED == easy->state) {
if(easy->easy_handle->dns.hostcachetype == HCACHE_MULTI) { if(data->dns.hostcachetype == HCACHE_MULTI) {
/* clear out the usage of the shared DNS cache */ /* clear out the usage of the shared DNS cache */
easy->easy_handle->dns.hostcache = NULL; data->dns.hostcache = NULL;
easy->easy_handle->dns.hostcachetype = HCACHE_NONE; data->dns.hostcachetype = HCACHE_NONE;
} }
/* now fill in the Curl_message with this info */ /* now fill in the Curl_message with this info */
msg = &easy->msg; msg = &easy->msg;
msg->extmsg.msg = CURLMSG_DONE; msg->extmsg.msg = CURLMSG_DONE;
msg->extmsg.easy_handle = easy->easy_handle; msg->extmsg.easy_handle = data;
msg->extmsg.data.result = easy->result; msg->extmsg.data.result = easy->result;
result = multi_addmsg(multi, msg); result = multi_addmsg(multi, msg);