implement retries for a few more commands

This commit is contained in:
Christian Grothoff 2018-08-11 11:29:02 +02:00
parent 1ee55ea838
commit 96c2fb8e10
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
5 changed files with 494 additions and 82 deletions

View File

@ -398,18 +398,20 @@ run (void *cls,
create_reserve_label,
AMOUNT_5,
MHD_HTTP_OK));
unit[1] = TALER_TESTING_cmd_deposit
("deposit",
is->exchange,
withdraw_label,
0, /* Index of the one withdrawn coin in the traits. */
TALER_TESTING_make_wire_details
(USER_ACCOUNT_NUMBER,
exchange_bank_account.hostname),
order_enc,
GNUNET_TIME_UNIT_ZERO,
AMOUNT_1,
MHD_HTTP_OK);
unit[1] =
TALER_TESTING_cmd_deposit_with_retry
(TALER_TESTING_cmd_deposit
("deposit",
is->exchange,
withdraw_label,
0, /* Index of the one withdrawn coin in the traits. */
TALER_TESTING_make_wire_details
(USER_ACCOUNT_NUMBER,
exchange_bank_account.hostname),
order_enc,
GNUNET_TIME_UNIT_ZERO,
AMOUNT_1,
MHD_HTTP_OK));
if (eval_probability (REFRESH_PROBABILITY))
{
@ -424,22 +426,28 @@ run (void *cls,
"refresh-reveal-%u-%u",
i,
j);
unit[2] = TALER_TESTING_cmd_refresh_melt
(melt_label,
is->exchange,
AMOUNT_4,
withdraw_label,
MHD_HTTP_OK);
unit[3] = TALER_TESTING_cmd_refresh_reveal
(reveal_label,
is->exchange,
melt_label,
MHD_HTTP_OK);
unit[4] = TALER_TESTING_cmd_refresh_link
("refresh-link",
is->exchange,
reveal_label,
MHD_HTTP_OK);
unit[2] =
TALER_TESTING_cmd_refresh_melt_with_retry
(TALER_TESTING_cmd_refresh_melt
(melt_label,
is->exchange,
AMOUNT_4,
withdraw_label,
MHD_HTTP_OK));
unit[3] =
TALER_TESTING_cmd_refresh_reveal_with_retry
(TALER_TESTING_cmd_refresh_reveal
(reveal_label,
is->exchange,
melt_label,
MHD_HTTP_OK));
unit[4] =
TALER_TESTING_cmd_refresh_link_with_retry
(TALER_TESTING_cmd_refresh_link
("refresh-link",
is->exchange,
reveal_label,
MHD_HTTP_OK));
unit[5] = TALER_TESTING_cmd_end ();
}
else

View File

@ -81,11 +81,6 @@ struct DepositState
*/
struct TALER_EXCHANGE_DepositHandle *dh;
/**
* Expected HTTP response code.
*/
unsigned int expected_response_code;
/**
* Interpreter state.
*/
@ -95,8 +90,60 @@ struct DepositState
* Exchange connection.
*/
struct TALER_EXCHANGE_Handle *exchange;
/**
* Task scheduled to try later.
*/
struct GNUNET_SCHEDULER_Task *retry_task;
/**
* How long do we wait until we retry?
*/
struct GNUNET_TIME_Relative backoff;
/**
* Expected HTTP response code.
*/
unsigned int expected_response_code;
/**
* Should we retry on (transient) failures?
*/
int do_retry;
};
/**
* Run the command.
*
* @param cls closure.
* @param cmd the command to execute.
* @param is the interpreter state.
*/
static void
deposit_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is);
/**
* Task scheduled to re-try #deposit_run.
*
* @param cls a `struct DepositState`
*/
static void
do_retry (void *cls)
{
struct DepositState *ds = cls;
ds->retry_task = NULL;
deposit_run (ds,
NULL,
ds->is);
}
/**
* Callback to analyze the /deposit response, just used to
* check if the response code is acceptable.
@ -120,6 +167,27 @@ deposit_cb (void *cls,
ds->dh = NULL;
if (ds->expected_response_code != http_status)
{
if (GNUNET_YES == ds->do_retry)
{
if ( (0 == http_status) ||
(TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec) ||
(MHD_HTTP_INTERNAL_SERVER_ERROR == http_status) )
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Retrying deposit failed with %u/%d\n",
http_status,
(int) ec);
/* on DB conflicts, do not use backoff */
if (TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec)
ds->backoff = GNUNET_TIME_UNIT_ZERO;
else
ds->backoff = GNUNET_TIME_STD_BACKOFF (ds->backoff);
ds->retry_task = GNUNET_SCHEDULER_add_delayed (ds->backoff,
&do_retry,
ds);
return;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u to command %s in %s:%u\n",
http_status,
@ -324,7 +392,11 @@ deposit_cleanup (void *cls,
TALER_EXCHANGE_deposit_cancel (ds->dh);
ds->dh = NULL;
}
if (NULL != ds->retry_task)
{
GNUNET_SCHEDULER_cancel (ds->retry_task);
ds->retry_task = NULL;
}
json_decref (ds->wire_details);
GNUNET_free (ds);
}
@ -441,3 +513,25 @@ TALER_TESTING_cmd_deposit
return cmd;
}
/**
* Modify a deposit command to enable retries when we get transient
* errors from the exchange.
*
* @param cmd a deposit command
* @return the command with retries enabled
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_deposit_with_retry (struct TALER_TESTING_Command cmd)
{
struct DepositState *ds;
GNUNET_assert (&deposit_run == cmd.run);
ds = cmd.cls;
ds->do_retry = GNUNET_YES;
return cmd;
}
/* end of testing_api_cmd_deposit.c */

View File

@ -55,19 +55,6 @@ struct MeltDetails
struct RefreshMeltState
{
/**
* if set to GNUNET_YES, then two /refresh/melt operations
* will be performed. This is needed to trigger the logic
* that manages those already-made requests. Note: it
* is not possible to just copy-and-paste a test refresh melt
* CMD to have the same effect, because every data preparation
* generates new planchets that (in turn) make the whole "hash"
* different from any previous one, therefore NOT allowing the
* exchange to pick any previous /rerfesh/melt operation from
* the database.
*/
unsigned int double_melt;
/**
* Information about coins to be melted.
*/
@ -78,11 +65,6 @@ struct RefreshMeltState
*/
char *refresh_data;
/**
* Number of bytes in @e refresh_data.
*/
size_t refresh_data_length;
/**
* Reference to a previous melt command.
*/
@ -103,16 +85,49 @@ struct RefreshMeltState
*/
struct TALER_TESTING_Interpreter *is;
/**
* Array of the denomination public keys
* corresponding to the @e fresh_amounts.
*/
struct TALER_EXCHANGE_DenomPublicKey *fresh_pks;
/**
* Task scheduled to try later.
*/
struct GNUNET_SCHEDULER_Task *retry_task;
/**
* How long do we wait until we retry?
*/
struct GNUNET_TIME_Relative backoff;
/**
* Number of bytes in @e refresh_data.
*/
size_t refresh_data_length;
/**
* Expected HTTP response code.
*/
unsigned int expected_response_code;
/**
* Array of the denomination public keys
* corresponding to the @e fresh_amounts.
* if set to #GNUNET_YES, then two /refresh/melt operations
* will be performed. This is needed to trigger the logic
* that manages those already-made requests. Note: it
* is not possible to just copy-and-paste a test refresh melt
* CMD to have the same effect, because every data preparation
* generates new planchets that (in turn) make the whole "hash"
* different from any previous one, therefore NOT allowing the
* exchange to pick any previous /rerfesh/melt operation from
* the database.
*/
struct TALER_EXCHANGE_DenomPublicKey *fresh_pks;
unsigned int double_melt;
/**
* Should we retry on (transient) failures?
*/
int do_retry;
/**
* Set by the melt callback as it comes from the exchange.
@ -136,13 +151,6 @@ struct RefreshRevealState
*/
struct TALER_EXCHANGE_RefreshRevealHandle *rrh;
/**
* Number of fresh coins withdrawn, set by the
* reveal callback as it comes from the exchange,
* it is the length of the @e fresh_coins array.
*/
unsigned int num_fresh_coins;
/**
* Convenience struct to keep in one place all the
* data related to one fresh coin, set by the reveal callback
@ -160,10 +168,33 @@ struct RefreshRevealState
*/
struct TALER_TESTING_Interpreter *is;
/**
* Task scheduled to try later.
*/
struct GNUNET_SCHEDULER_Task *retry_task;
/**
* How long do we wait until we retry?
*/
struct GNUNET_TIME_Relative backoff;
/**
* Number of fresh coins withdrawn, set by the
* reveal callback as it comes from the exchange,
* it is the length of the @e fresh_coins array.
*/
unsigned int num_fresh_coins;
/**
* Expected HTTP response code.
*/
unsigned int expected_response_code;
/**
* Should we retry on (transient) failures?
*/
int do_retry;
};
@ -192,13 +223,59 @@ struct RefreshLinkState
*/
struct TALER_TESTING_Interpreter *is;
/**
* Task scheduled to try later.
*/
struct GNUNET_SCHEDULER_Task *retry_task;
/**
* How long do we wait until we retry?
*/
struct GNUNET_TIME_Relative backoff;
/**
* Expected HTTP response code.
*/
unsigned int expected_response_code;
/**
* Should we retry on (transient) failures?
*/
int do_retry;
};
/**
* Run the command.
*
* @param cls closure.
* @param cmd the command to execute.
* @param is the interpreter state.
*/
static void
refresh_reveal_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is);
/**
* Task scheduled to re-try #refresh_reveal_run.
*
* @param cls a `struct RefreshRevealState`
*/
static void
do_reveal_retry (void *cls)
{
struct RefreshRevealState *rrs = cls;
rrs->retry_task = NULL;
refresh_reveal_run (rrs,
NULL,
rrs->is);
}
/**
* "refresh reveal" request callback; it checks that the response
* code is expected and copies into its command's state the data
@ -231,6 +308,27 @@ reveal_cb (void *cls,
rrs->rrh = NULL;
if (rrs->expected_response_code != http_status)
{
if (GNUNET_YES == rrs->do_retry)
{
if ( (0 == http_status) ||
(TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec) ||
(MHD_HTTP_INTERNAL_SERVER_ERROR == http_status) )
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Retrying refresh reveal failed with %u/%d\n",
http_status,
(int) ec);
/* on DB conflicts, do not use backoff */
if (TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec)
rrs->backoff = GNUNET_TIME_UNIT_ZERO;
else
rrs->backoff = GNUNET_TIME_STD_BACKOFF (rrs->backoff);
rrs->retry_task = GNUNET_SCHEDULER_add_delayed (rrs->backoff,
&do_reveal_retry,
rrs);
return;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u/%d to command %s in %s:%u\n",
http_status,
@ -258,16 +356,18 @@ reveal_cb (void *cls,
(num_coins, struct FreshCoin);
const struct TALER_EXCHANGE_DenomPublicKey *fresh_pks;
unsigned int i;
if (GNUNET_OK != TALER_TESTING_get_trait_denom_pub
(melt_cmd, 0, &fresh_pks))
if (GNUNET_OK !=
TALER_TESTING_get_trait_denom_pub (melt_cmd,
0,
&fresh_pks))
{
GNUNET_break (0);
TALER_TESTING_interpreter_fail (rrs->is);
return;
}
for (i=0; i<num_coins; i++)
for (unsigned int i=0; i<num_coins; i++)
{
struct FreshCoin *fc = &rrs->fresh_coins[i];
@ -352,6 +452,11 @@ refresh_reveal_cleanup (void *cls,
TALER_EXCHANGE_refresh_reveal_cancel (rrs->rrh);
rrs->rrh = NULL;
}
if (NULL != rrs->retry_task)
{
GNUNET_SCHEDULER_cancel (rrs->retry_task);
rrs->retry_task = NULL;
}
for (unsigned int j=0; j < rrs->num_fresh_coins; j++)
GNUNET_CRYPTO_rsa_signature_free (rrs->fresh_coins[j].sig.rsa_signature);
@ -362,6 +467,36 @@ refresh_reveal_cleanup (void *cls,
}
/**
* Run the command.
*
* @param cls closure.
* @param cmd the command to execute.
* @param is the interpreter state.
*/
static void
refresh_link_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is);
/**
* Task scheduled to re-try #refresh_link_run.
*
* @param cls a `struct RefreshLinkState`
*/
static void
do_link_retry (void *cls)
{
struct RefreshLinkState *rls = cls;
rls->retry_task = NULL;
refresh_link_run (rls,
NULL,
rls->is);
}
/**
* "refresh link" operation callback, checks that HTTP response
* code is expected _and_ that all the linked coins were actually
@ -402,6 +537,27 @@ link_cb (void *cls,
rls->rlh = NULL;
if (rls->expected_response_code != http_status)
{
if (GNUNET_YES == rls->do_retry)
{
if ( (0 == http_status) ||
(TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec) ||
(MHD_HTTP_INTERNAL_SERVER_ERROR == http_status) )
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Retrying refresh link failed with %u/%d\n",
http_status,
(int) ec);
/* on DB conflicts, do not use backoff */
if (TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec)
rls->backoff = GNUNET_TIME_UNIT_ZERO;
else
rls->backoff = GNUNET_TIME_STD_BACKOFF (rls->backoff);
rls->retry_task = GNUNET_SCHEDULER_add_delayed (rls->backoff,
&do_link_retry,
rls);
return;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u/%d to command %s in %s:%u\n",
http_status,
@ -514,11 +670,9 @@ refresh_link_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is)
{
struct RefreshLinkState *rls = cls;
struct RefreshRevealState *rrs;
struct RefreshMeltState *rms;
const struct TALER_TESTING_Command *reveal_cmd;
const struct TALER_TESTING_Command *melt_cmd;
const struct TALER_TESTING_Command *coin_cmd;
@ -605,6 +759,41 @@ refresh_link_cleanup (void *cls,
TALER_EXCHANGE_refresh_link_cancel (rls->rlh);
rls->rlh = NULL;
}
if (NULL != rls->retry_task)
{
GNUNET_SCHEDULER_cancel (rls->retry_task);
rls->retry_task = NULL;
}
}
/**
* Run the command.
*
* @param cls closure.
* @param cmd the command to execute.
* @param is the interpreter state.
*/
static void
refresh_melt_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is);
/**
* Task scheduled to re-try #refresh_melt_run.
*
* @param cls a `struct RefreshMeltState`
*/
static void
do_melt_retry (void *cls)
{
struct RefreshMeltState *rms = cls;
rms->retry_task = NULL;
refresh_melt_run (rms,
NULL,
rms->is);
}
@ -634,6 +823,27 @@ melt_cb (void *cls,
rms->rmh = NULL;
if (rms->expected_response_code != http_status)
{
if (GNUNET_YES == rms->do_retry)
{
if ( (0 == http_status) ||
(TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec) ||
(MHD_HTTP_INTERNAL_SERVER_ERROR == http_status) )
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Retrying refresh melt failed with %u/%d\n",
http_status,
(int) ec);
/* on DB conflicts, do not use backoff */
if (TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec)
rms->backoff = GNUNET_TIME_UNIT_ZERO;
else
rms->backoff = GNUNET_TIME_STD_BACKOFF (rms->backoff);
rms->retry_task = GNUNET_SCHEDULER_add_delayed (rms->backoff,
&do_melt_retry,
rms);
return;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u/%d to command %s in %s:%u\n",
http_status,
@ -668,7 +878,7 @@ melt_cb (void *cls,
* @param cmd the command to execute.
* @param is the interpreter state.
*/
void
static void
refresh_melt_run (void *cls,
const struct TALER_TESTING_Command *cmd,
struct TALER_TESTING_Interpreter *is)
@ -819,6 +1029,11 @@ refresh_melt_cleanup (void *cls,
TALER_EXCHANGE_refresh_melt_cancel (rms->rmh);
rms->rmh = NULL;
}
if (NULL != rms->retry_task)
{
GNUNET_SCHEDULER_cancel (rms->retry_task);
rms->retry_task = NULL;
}
GNUNET_free_non_null (rms->fresh_pks);
rms->fresh_pks = NULL;
GNUNET_free_non_null (rms->refresh_data);
@ -894,10 +1109,10 @@ TALER_TESTING_cmd_refresh_melt
cmd.run = &refresh_melt_run;
cmd.cleanup = &refresh_melt_cleanup;
cmd.traits = &refresh_melt_traits;
return cmd;
}
/**
* Create a "refresh melt" CMD that does TWO /refresh/melt
* requests. This was needed to test the replay of a valid melt
@ -938,10 +1153,28 @@ TALER_TESTING_cmd_refresh_melt_double
cmd.run = &refresh_melt_run;
cmd.cleanup = &refresh_melt_cleanup;
cmd.traits = &refresh_melt_traits;
return cmd;
}
/**
* Modify a "refresh melt" command to enable retries.
*
* @param cmd command
* @return modified command.
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_refresh_melt_with_retry (struct TALER_TESTING_Command cmd)
{
struct RefreshMeltState *rms;
GNUNET_assert (&refresh_melt_run == cmd.run);
rms = cmd.cls;
rms->do_retry = GNUNET_YES;
return cmd;
}
/**
* Offer internal data from a "refresh reveal" CMD.
*
@ -960,23 +1193,22 @@ refresh_reveal_traits (void *cls,
{
struct RefreshRevealState *rrs = cls;
unsigned int num_coins = rrs->num_fresh_coins;
#define NUM_TRAITS (num_coins * 3) + 3
#define NUM_TRAITS (num_coins * 3) + 3
struct TALER_TESTING_Trait traits[NUM_TRAITS];
unsigned int i;
/* Making coin privs traits */
for (i=0; i<num_coins; i++)
for (unsigned int i=0; i<num_coins; i++)
traits[i] = TALER_TESTING_make_trait_coin_priv
(i, &rrs->fresh_coins[i].coin_priv);
/* Making denom pubs traits */
for (i=0; i<num_coins; i++)
for (unsigned int i=0; i<num_coins; i++)
traits[num_coins + i]
= TALER_TESTING_make_trait_denom_pub
(i, rrs->fresh_coins[i].pk);
/* Making denom sigs traits */
for (i=0; i<num_coins; i++)
for (unsigned int i=0; i<num_coins; i++)
traits[(num_coins * 2) + i]
= TALER_TESTING_make_trait_denom_sig
(i, &rrs->fresh_coins[i].sig);
@ -998,6 +1230,7 @@ refresh_reveal_traits (void *cls,
index);
}
/**
* Create a "refresh reveal" command.
*
@ -1028,7 +1261,24 @@ TALER_TESTING_cmd_refresh_reveal
cmd.run = &refresh_reveal_run;
cmd.cleanup = &refresh_reveal_cleanup;
cmd.traits = &refresh_reveal_traits;
return cmd;
}
/**
* Modify a "refresh reveal" command to enable retries.
*
* @param cmd command
* @return modified command.
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_refresh_reveal_with_retry (struct TALER_TESTING_Command cmd)
{
struct RefreshRevealState *rrs;
GNUNET_assert (&refresh_reveal_run == cmd.run);
rrs = cmd.cls;
rrs->do_retry = GNUNET_YES;
return cmd;
}
@ -1062,6 +1312,23 @@ TALER_TESTING_cmd_refresh_link
cmd.label = label;
cmd.run = &refresh_link_run;
cmd.cleanup = &refresh_link_cleanup;
return cmd;
}
/**
* Modify a "refresh link" command to enable retries.
*
* @param cmd command
* @return modified command.
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_refresh_link_with_retry (struct TALER_TESTING_Command cmd)
{
struct RefreshLinkState *rls;
GNUNET_assert (&refresh_link_run == cmd.run);
rls = cmd.cls;
rls->do_retry = GNUNET_YES;
return cmd;
}

View File

@ -169,15 +169,16 @@ reserve_withdraw_cb (void *cls,
{
if (GNUNET_YES == ws->do_retry)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Retrying withdraw failed with %u/%d\n",
http_status,
(int) ec);
if ( (TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec) ||
if ( (0 == http_status) ||
(TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec) ||
(TALER_EC_WITHDRAW_INSUFFICIENT_FUNDS == ec) ||
(TALER_EC_WITHDRAW_RESERVE_UNKNOWN == ec) ||
(MHD_HTTP_INTERNAL_SERVER_ERROR == http_status) )
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Retrying withdraw failed with %u/%d\n",
http_status,
(int) ec);
/* on DB conflicts, do not use backoff */
if (TALER_EC_DB_COMMIT_FAILED_ON_RETRY == ec)
ws->backoff = GNUNET_TIME_UNIT_ZERO;

View File

@ -835,6 +835,17 @@ TALER_TESTING_cmd_deposit
unsigned int expected_response_code);
/**
* Modify a deposit command to enable retries when we get transient
* errors from the exchange.
*
* @param cmd a deposit command
* @return the command with retries enabled
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_deposit_with_retry (struct TALER_TESTING_Command cmd);
/**
* Create a "refresh melt" command.
*
@ -855,6 +866,7 @@ TALER_TESTING_cmd_refresh_melt
const char *coin_reference,
unsigned int expected_response_code);
/**
* Create a "refresh melt" CMD that does TWO /refresh/melt
* requests. This was needed to test the replay of a valid melt
@ -878,6 +890,16 @@ TALER_TESTING_cmd_refresh_melt_double
unsigned int expected_response_code);
/**
* Modify a "refresh melt" command to enable retries.
*
* @param cmd command
* @return modified command.
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_refresh_melt_with_retry (struct TALER_TESTING_Command cmd);
/**
* Create a "refresh reveal" command.
*
@ -896,6 +918,16 @@ TALER_TESTING_cmd_refresh_reveal
unsigned int expected_response_code);
/**
* Modify a "refresh reveal" command to enable retries.
*
* @param cmd command
* @return modified command.
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_refresh_reveal_with_retry (struct TALER_TESTING_Command cmd);
/**
* Create a "refresh link" command.
*
@ -914,6 +946,16 @@ TALER_TESTING_cmd_refresh_link
unsigned int expected_response_code);
/**
* Modify a "refresh link" command to enable retries.
*
* @param cmd command
* @return modified command.
*/
struct TALER_TESTING_Command
TALER_TESTING_cmd_refresh_link_with_retry (struct TALER_TESTING_Command cmd);
/**
* Create a "track transaction" command.
*