Hi Luis,
Thx for the review. See comments in-line.
Cheers.
On 06/03/2011 11:22 AM, Luís Soares wrote:
> Hi Alfranio,
>
> Great job. We're getting there, but there is a little more
> work left. Please find my review comments below.
>
> STATUS
> ------
>
> Not approved.
>
> REQUIRED CHANGES
> ----------------
>
> RC1. We need a test case, at least to test that mysqldump will
> work.
I agree. After getting some feedback from Sven, I will commit a new patch with a test
case.
>
> RC2. void check_access_rpl_repository_after(THD *thd)
>
> The comments for this function state:
>
> +/**
> + Cleans up after the check_access_rpl_repository_after.
>
> Is it really after the check_access_rpl_repository_after? Or is
> it check_access_rpl_repository_before ?
ok.
>
> RC3. void check_access_rpl_repository_after(THD *thd)
>
> This:
>
> DBUG_ENTER("check_access_rpl_repository")
>
> should be:
>
> DBUG_ENTER("check_access_rpl_repository_after")
>
> Same for check_access_rpl_repository_before.
>
> Actually, I think that these functions should have
> different names. Something like:
>
> check_and_lock_slave_on_rpl_repo_access()
> unlock_slave_on_rpl_repo_access()
ok.
>
> RC4. Please, move this:
>
> + /*
> + Notify user that the command cannot be executed against
> + replication tables while slave is running.
> + */
> + my_error(ER_SLAVE_MUST_STOP, MYF(0));
>
> from check_access_rpl_repository_before to when the check
> function is called in mysql_execute_command (see the part
> that calls deny_updates_if_read_only_option).
>
ok.
> RC5. in check_access_rpl_repository_before, why don't we
> clean up (unlock) on error, before returning true ? If
> we did that, we could just (and considering RC4):
>
> if (check_access_rpl_repository_before(thd))
> {
> /*
> Notify user that the command cannot be executed against
> replication tables while slave is running.
> */
> my_error(ER_SLAVE_MUST_STOP, MYF(0));
> DBUG_RETURN(-1);
> }
ok.
>
> REQUESTS
> --------
>
> R1. Have you checked that lower_case_table_names= 2 will work
> properly with check_if_rpl_table ?
Yes. I will commit a new patch with a test case.
>
> SUGGESTIONS
> -----------
>
> S1. bool check_access_rpl_repository_before(THD *thd)
>
> This function returns 'bool', thus I think we should return
> true/false instead of TRUE/FALSE.
>
> S2. I suggest you run this through runtime.
I will wait for Sven's comment, create a new patch and ask someone from runtime to look at
it.
>
> DETAILS
> -------
> n/a
>
>
> On 05/31/2011 11:05 PM, Alfranio Correia wrote:
>> #At
> file:///home/acorreia/workspace.oracle/repository.mysql/bzrwork/bug-60902/mysql-trunk/
> based on revid:bjorn.munch@stripped
>>
>> 3125 Alfranio Correia 2011-05-31
>> BUG#12402875 - BUG#60902: MYSQLDUMP DOES NOT DUMP THE REPLICATION *_INFO
> TABLES
>>
>> When WL#2775 was developed you have decided to follow the pattern used by
>> log tables where direct updates on them re forbidden. Then later we would
>> provide users with the necessary commands to execute the maintenance
> tasks
>> such as changing the engine in use or dumping data.
>>
>> However, we have realized that this would unnecessarily duplicate code,
>> i.e. functionality.
>>
>> So in this patch, we revert the code that follows the behavior adopt by
>> log tables and allow any sort of statement to be executed against the
>> replication tables provided that users have the appropriate permissions
>> and replication is not running. Read operations such as a SELECT can be
>> concurrently executed.
>> @ client/mysqldump.c
>> Removed the code that ignored replication tables while running
> mysqldump.
>> @ sql/lock.cc
>> Removed checks that automatically forbid write locks on replication
>> tables what was borrowed from log tables.
>> @ sql/rpl_info_file.cc
>> Removed this code that was introduced just to track down BUG#11766392.
>> However, we have figured that the problem was an environment issue.
>> @ sql/sql_class.h
>> CF_WRITE_RPL_INFO_COMMAND was removed because any update to a
>> replication table is forbidden while replication is running.
>> @ sql/sql_parse.cc
>> The file has:
>> . augmented the SQL command flag to indicate if one can execute
>> a statement against a replication table while replication is
>> running. Currently only reads, e.g. SELECT, are allowed.
>>
>> . CF_WRITE_RPL_INFO_COMMAND was removed because any update to a
>> replication table is forbidden while replication is running.
>>
>> . Introduced "check_access_rpl_repository_before" what checks
>> if an SQL statment can be executed against a replication
>> table. If a replication table is being accessed a lock is
>> acquired on LOCK_active_mi and active_mi mutexes.
>>
>> . Introduced "check_access_rpl_repository_after" that releases
>> any lock acquired at "check_access_rpl_repository_before".
>> @ sql/sql_parse.h
>> Created a function to check if the SQL command flag is tagged
>> with CF_ACCESS_RPL_INFO_COMMAND.
>>
>> This means that the SQL statement can access, currently read,
>> replication tables while replication is running.
>> @ sql/sql_table.cc
>> Created a function to check if a table belongs to the set of
>> replication tables.
>> @ sql/sql_table.h
>> Created a function to check if a table belongs to the set of
>> replication tables.
>>
>> modified:
>> client/mysqldump.c
>> sql/lock.cc
>> sql/rpl_info_file.cc
>> sql/sql_class.h
>> sql/sql_parse.cc
>> sql/sql_parse.h
>> sql/sql_table.cc
>> sql/sql_table.h
>> === modified file 'client/mysqldump.c'
>> --- a/client/mysqldump.c 2011-05-26 15:20:09 +0000
>> +++ b/client/mysqldump.c 2011-05-31 22:05:19 +0000
>> @@ -910,11 +910,7 @@ static int get_options(int *argc, char *
>> my_hash_insert(&ignore_table,
>> (uchar*) my_strdup("mysql.general_log", MYF(MY_WME))) ||
>> my_hash_insert(&ignore_table,
>> - (uchar*) my_strdup("mysql.slow_log", MYF(MY_WME))) ||
>> - my_hash_insert(&ignore_table,
>> - (uchar*) my_strdup("mysql.slave_master_info", MYF(MY_WME)))
> ||
>> - my_hash_insert(&ignore_table,
>> - (uchar*) my_strdup("mysql.slave_relay_log_info",
> MYF(MY_WME))))
>> + (uchar*) my_strdup("mysql.slow_log", MYF(MY_WME))))
>> return(EX_EOM);
>>
>> if ((ho_error= handle_options(argc, argv, my_long_options, get_one_option)))
>>
>> === modified file 'sql/lock.cc'
>> --- a/sql/lock.cc 2011-05-04 09:54:04 +0000
>> +++ b/sql/lock.cc 2011-05-31 22:05:19 +0000
>> @@ -121,7 +121,7 @@ lock_tables_check(THD *thd, TABLE **tabl
>> Identifies if the executed sql command can updated either a log
>> or rpl info table.
>> */
>> - bool log_table_write_query, rpl_info_table_write_query;
>> + bool log_table_write_query;
>>
>> DBUG_ENTER("lock_tables_check");
>>
>> @@ -129,8 +129,6 @@ lock_tables_check(THD *thd, TABLE **tabl
>> is_superuser= thd->security_ctx->master_access& SUPER_ACL;
>> log_table_write_query=
>> is_log_table_write_query(thd->lex->sql_command);
>> - rpl_info_table_write_query=
>> - is_rpl_info_table_write_query(thd->lex->sql_command);
>>
>> for (i=0 ; i<count; i++)
>> {
>> @@ -145,23 +143,6 @@ lock_tables_check(THD *thd, TABLE **tabl
>> When a user is requesting a lock, the following
>> constraints are enforced:
>> */
>> - if (t->s->table_category == TABLE_CATEGORY_RPL_INFO&&
>> - (flags& MYSQL_LOCK_RPL_INFO_TABLE) == 0&&
>> - !rpl_info_table_write_query)
>> - {
>> - /*
>> - A user should not be able to prevent writes,
>> - or hold any type of lock in a session,
>> - since this would be a DOS attack.
>> - */
>> - if (t->reginfo.lock_type>= TL_READ_NO_INSERT ||
>> - thd->lex->sql_command == SQLCOM_LOCK_TABLES)
>> - {
>> - my_error(ER_CANT_LOCK_RPL_INFO_TABLE, MYF(0));
>> - DBUG_RETURN(1);
>> - }
>> - }
>> -
>> if (t->s->table_category == TABLE_CATEGORY_LOG&&
>> (flags& MYSQL_LOCK_LOG_TABLE) == 0&&
>> !log_table_write_query)
>>
>> === modified file 'sql/rpl_info_file.cc'
>> --- a/sql/rpl_info_file.cc 2011-05-26 15:20:09 +0000
>> +++ b/sql/rpl_info_file.cc 2011-05-31 22:05:19 +0000
>> @@ -126,25 +126,6 @@ int Rpl_info_file::do_prepare_info_for_w
>>
>> int Rpl_info_file::do_check_info()
>> {
>> - /*
>> - This function checks if the file exists and in other modules
>> - further actions are taken based on this. If the file exists
>> - but users do not have the appropriated rights to access it,
>> - other modules will assume that the file does not exist and
>> - will take the appropriate actions and most likely will fail
>> - safely after trying to create it.
>> -
>> - This is behavior is not a problem. However, in other modules,
>> - it is not possible to print out what is the root of the
>> - failure as a detailed error code is not returned. For that
>> - reason, we print out such information in here.
>> - */
>> - if (my_access(info_fname, F_OK | R_OK | W_OK))
>> - sql_print_information("Info file %s cannot be accessed (errno %d)."
>> - " Most likely this is a new slave or you are "
>> - " changing the repository type.", info_fname,
>> - errno);
>> -
>> return my_access(info_fname, F_OK);
>> }
>>
>>
>> === modified file 'sql/sql_class.h'
>> --- a/sql/sql_class.h 2011-05-26 15:20:09 +0000
>> +++ b/sql/sql_class.h 2011-05-31 22:05:19 +0000
>> @@ -4052,9 +4052,10 @@ public:
>> #define CF_HA_CLOSE (1U<< 11)
>>
>> /**
>> - Identifies statements that can directly update a rpl info table.
>> + Identifies statements that can access a rpl info table while replication
>> + is running.
>> */
>> -#define CF_WRITE_RPL_INFO_COMMAND (1U<< 12)
>> +#define CF_ACCESS_RPL_INFO_COMMAND (1U<< 12)
>>
>> /* Bits in server_command_flags */
>>
>>
>> === modified file 'sql/sql_parse.cc'
>> --- a/sql/sql_parse.cc 2011-05-26 15:20:09 +0000
>> +++ b/sql/sql_parse.cc 2011-05-31 22:05:19 +0000
>> @@ -117,6 +117,10 @@ static bool execute_sqlcom_select(THD *t
>> static bool check_show_access(THD *thd, TABLE_LIST *table);
>> static void sql_kill(THD *thd, ulong id, bool only_kill_query);
>> static bool lock_tables_precheck(THD *thd, TABLE_LIST *tables);
>> +#ifdef HAVE_REPLICATION
>> +static bool check_access_rpl_repository_before(THD *thd);
>> +static void check_access_rpl_repository_after(THD *thd);
>> +#endif
>>
>> const char *any_db="*any*"; // Special symbol for check_access
>>
>> @@ -300,8 +304,7 @@ void init_update_queries(void)
>> CF_CAN_GENERATE_ROW_EVENTS;
>> sql_command_flags[SQLCOM_CREATE_INDEX]= CF_CHANGES_DATA |
> CF_AUTO_COMMIT_TRANS;
>> sql_command_flags[SQLCOM_ALTER_TABLE]= CF_CHANGES_DATA |
> CF_WRITE_LOGS_COMMAND |
>> - CF_AUTO_COMMIT_TRANS |
>> - CF_WRITE_RPL_INFO_COMMAND;
>> + CF_AUTO_COMMIT_TRANS;
>> sql_command_flags[SQLCOM_TRUNCATE]= CF_CHANGES_DATA |
> CF_WRITE_LOGS_COMMAND |
>> CF_AUTO_COMMIT_TRANS;
>> sql_command_flags[SQLCOM_DROP_TABLE]= CF_CHANGES_DATA |
> CF_AUTO_COMMIT_TRANS;
>> @@ -339,7 +342,8 @@ void init_update_queries(void)
>> sql_command_flags[SQLCOM_REPLACE_SELECT]= CF_CHANGES_DATA |
> CF_REEXECUTION_FRAGILE |
>> CF_CAN_GENERATE_ROW_EVENTS;
>> sql_command_flags[SQLCOM_SELECT]= CF_REEXECUTION_FRAGILE |
>> - CF_CAN_GENERATE_ROW_EVENTS;
>> + CF_CAN_GENERATE_ROW_EVENTS |
>> + CF_ACCESS_RPL_INFO_COMMAND;
>> sql_command_flags[SQLCOM_SET_OPTION]= CF_REEXECUTION_FRAGILE |
> CF_AUTO_COMMIT_TRANS;
>> sql_command_flags[SQLCOM_DO]= CF_REEXECUTION_FRAGILE |
>> CF_CAN_GENERATE_ROW_EVENTS;
>> @@ -371,7 +375,7 @@ void init_update_queries(void)
>> sql_command_flags[SQLCOM_SHOW_PROCESSLIST]= CF_STATUS_COMMAND;
>> sql_command_flags[SQLCOM_SHOW_GRANTS]= CF_STATUS_COMMAND;
>> sql_command_flags[SQLCOM_SHOW_CREATE_DB]= CF_STATUS_COMMAND;
>> - sql_command_flags[SQLCOM_SHOW_CREATE]= CF_STATUS_COMMAND;
>> + sql_command_flags[SQLCOM_SHOW_CREATE]= CF_STATUS_COMMAND |
> CF_ACCESS_RPL_INFO_COMMAND;
>> sql_command_flags[SQLCOM_SHOW_MASTER_STAT]= CF_STATUS_COMMAND;
>> sql_command_flags[SQLCOM_SHOW_SLAVE_STAT]= CF_STATUS_COMMAND;
>> sql_command_flags[SQLCOM_SHOW_CREATE_PROC]= CF_STATUS_COMMAND;
>> @@ -392,7 +396,6 @@ void init_update_queries(void)
>> CF_SHOW_TABLE_COMMAND |
>> CF_REEXECUTION_FRAGILE);
>>
>> -
>> sql_command_flags[SQLCOM_CREATE_USER]= CF_CHANGES_DATA;
>> sql_command_flags[SQLCOM_RENAME_USER]= CF_CHANGES_DATA;
>> sql_command_flags[SQLCOM_DROP_USER]= CF_CHANGES_DATA;
>> @@ -423,14 +426,10 @@ void init_update_queries(void)
>> The following admin table operations are allowed
>> on log tables.
>> */
>> - sql_command_flags[SQLCOM_REPAIR]= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS |
>> - CF_WRITE_RPL_INFO_COMMAND;
>> - sql_command_flags[SQLCOM_OPTIMIZE]|= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS |
>> - CF_WRITE_RPL_INFO_COMMAND;
>> - sql_command_flags[SQLCOM_ANALYZE]= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS |
>> - CF_WRITE_RPL_INFO_COMMAND;
>> - sql_command_flags[SQLCOM_CHECK]= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS |
>> - CF_WRITE_RPL_INFO_COMMAND;
>> + sql_command_flags[SQLCOM_REPAIR]= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS;
>> + sql_command_flags[SQLCOM_OPTIMIZE]|= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS;
>> + sql_command_flags[SQLCOM_ANALYZE]= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS;
>> + sql_command_flags[SQLCOM_CHECK]= CF_WRITE_LOGS_COMMAND |
> CF_AUTO_COMMIT_TRANS;
>>
>> sql_command_flags[SQLCOM_CREATE_USER]|= CF_AUTO_COMMIT_TRANS;
>> sql_command_flags[SQLCOM_DROP_USER]|= CF_AUTO_COMMIT_TRANS;
>> @@ -529,14 +528,14 @@ bool is_log_table_write_query(enum enum_
>> }
>>
>> /**
>> - Check if a sql command is allowed to write to rpl info tables.
>> + Check if a sql command is allowed to access rpl info tables.
>> @param command The SQL command
>> - @return true if writing is allowed
>> + @return true if access is allowed
>> */
>> -bool is_rpl_info_table_write_query(enum enum_sql_command command)
>> +bool is_rpl_info_table_access_query(enum enum_sql_command command)
>> {
>> DBUG_ASSERT(command>= 0&& command<= SQLCOM_END);
>> - return (sql_command_flags[command]& CF_WRITE_RPL_INFO_COMMAND) != 0;
>> + return (sql_command_flags[command]& CF_ACCESS_RPL_INFO_COMMAND) != 0;
>> }
>>
>> void execute_init_command(THD *thd, LEX_STRING *init_command,
>> @@ -2156,6 +2155,17 @@ mysql_execute_command(THD *thd)
>> }
>> #ifdef HAVE_REPLICATION
>> } /* endif unlikely slave */
>> +
>> + /*
>> + This function must be moved to another module, transformed in a
>> + callback or refactored after WL#5675. Maybe the same should be
>> + applied to the code the follows this function.
>> + */
>> + if (check_access_rpl_repository_before(thd))
>> + {
>> + check_access_rpl_repository_after(thd);
>> + DBUG_RETURN(-1);
>> + }
>> #endif
>>
>> status_var_increment(thd->status_var.com_stat[lex->sql_command]);
>> @@ -4586,6 +4596,17 @@ finish:
>> thd->mdl_context.release_statement_locks();
>> }
>>
>> +#ifdef HAVE_REPLICATION
>> + /*
>> + This function must be moved to another module, transformed in a
>> + callback or refactored after WL#5675.
>> + This function is not supposed to fail and must just reset the
>> + flag in replication, if necessary, in order to allow replication
>> + to start.
>> + */
>> + check_access_rpl_repository_after(thd);
>> +#endif
>> +
>> DBUG_RETURN(res || thd->is_error());
>> }
>>
>> @@ -7629,3 +7650,83 @@ merge_charset_and_collation(const CHARSE
>> }
>> return cs;
>> }
>> +
>> +#ifdef HAVE_REPLICATION
>> +/**
>> + Checks if it is possible to access a replication table while
>> + replicaiton is running. Currently, it is only possible to
>> + read such tables.
>> +
>> + If a replication table is accessed and replication is not
>> + running the LOCK_active_mi and active_mi mutexes are locked
>> + while running the statement.
>> +
>> + @param thd Pointer to the current thread.
>> +
>> + @return true if it is not possible, false, otherwise.
>> +*/
>> +bool check_access_rpl_repository_before(THD *thd)
>> +{
>> + int running= 0;
>> + LEX* lex= thd->lex;
>> + TABLE_LIST *table= NULL;
>> + DBUG_ENTER("check_access_rpl_repository");
>> +
>> + if (!is_rpl_info_table_access_query(lex->sql_command))
>> + {
>> + for (table= lex->query_tables; table; table= table->next_local)
>> + {
>> + if (check_if_rpl_table(table->db_length, table->db,
>> + table->table_name_length,
>> + table->table_name))
>> + {
>> + mysql_mutex_lock(&LOCK_active_mi);
>> + lock_slave_threads(active_mi);
>> + init_thread_mask(&running, active_mi, FALSE);
>> + if (running)
>> + {
>> + /*
>> + Notify user that the command cannot be executed against
>> + replication tables while slave is running.
>> + */
>> + my_error(ER_SLAVE_MUST_STOP, MYF(0));
>> + DBUG_RETURN(TRUE);
>> + }
>> + DBUG_RETURN(FALSE);
>> + }
>> + }
>> + }
>> + DBUG_RETURN(FALSE);
>> +}
>> +
>> +/**
>> + Cleans up after the check_access_rpl_repository_after.
>> +
>> + Basically, it releases the locks on the LOCK_active_mi and
>> + active_mi if necessary.
>> +
>> + @param thd Pointer to the current thread.
>> +*/
>> +void check_access_rpl_repository_after(THD *thd)
>> +{
>> + LEX* lex= thd->lex;
>> + TABLE_LIST *table= NULL;
>> + DBUG_ENTER("check_access_rpl_repository");
>> +
>> + if (!is_rpl_info_table_access_query(lex->sql_command))
>> + {
>> + for (table= lex->query_tables; table; table= table->next_local)
>> + {
>> + if (check_if_rpl_table(table->db_length, table->db,
>> + table->table_name_length,
>> + table->table_name))
>> + {
>> + unlock_slave_threads(active_mi);
>> + mysql_mutex_unlock(&LOCK_active_mi);
>> + DBUG_VOID_RETURN;
>> + }
>> + }
>> + }
>> + DBUG_VOID_RETURN;
>> +}
>> +#endif
>>
>> === modified file 'sql/sql_parse.h'
>> --- a/sql/sql_parse.h 2011-05-04 07:51:15 +0000
>> +++ b/sql/sql_parse.h 2011-05-31 22:05:19 +0000
>> @@ -83,7 +83,7 @@ bool check_identifier_name(LEX_STRING *s
>> bool mysql_test_parse_for_slave(THD *thd,char *inBuf,uint length);
>> bool is_update_query(enum enum_sql_command command);
>> bool is_log_table_write_query(enum enum_sql_command command);
>> -bool is_rpl_info_table_write_query(enum enum_sql_command command);
>> +bool is_rpl_info_table_access_query(enum enum_sql_command command);
>> bool alloc_query(THD *thd, const char *packet, uint packet_length);
>> void mysql_init_select(LEX *lex);
>> void mysql_parse(THD *thd, char *rawbuf, uint length,
>>
>> === modified file 'sql/sql_table.cc'
>> --- a/sql/sql_table.cc 2011-05-26 15:20:09 +0000
>> +++ b/sql/sql_table.cc 2011-05-31 22:05:19 +0000
>> @@ -7556,3 +7556,40 @@ static bool check_engine(THD *thd, const
>> }
>> return FALSE;
>> }
>> +
>> +/**
>> + Checks if db.table_name is a replication table, i.e. MI_INFO_NAME or
>> + RLI_INFO_NAME.
>> +
>> + @param db_len Size of database's name.
>> + @param db Database's name.
>> + @param table_name_len Size of the table's name.
>> + @param table_name Table's name.
>> +
>> + @return 1, if db.table is a replication table. 0, otherwise.
>> +*/
>> +int check_if_rpl_table(size_t db_len, const char *db, size_t table_name_len,
>> + const char *table_name)
>> +{
>> + if (db_len == MYSQL_SCHEMA_NAME.length&&
>> + !(lower_case_table_names ?
>> + my_strcasecmp(system_charset_info, db, MYSQL_SCHEMA_NAME.str) :
>> + strcmp(db, MYSQL_SCHEMA_NAME.str)))
>> + {
>> + if (table_name_len == RLI_INFO_NAME.length&&
> !(lower_case_table_names ?
>> + my_strcasecmp(system_charset_info,
>> + table_name, RLI_INFO_NAME.str)
> :
>> + strcmp(table_name, RLI_INFO_NAME.str)))
>> + {
>> + return 1;
>> + }
>> +
>> + if (table_name_len == MI_INFO_NAME.length&&
> !(lower_case_table_names ?
>> + my_strcasecmp(system_charset_info, table_name, MI_INFO_NAME.str) :
>> + strcmp(table_name, MI_INFO_NAME.str)))
>> + {
>> + return 1;
>> + }
>> + }
>> + return 0;
>> +}
>>
>> === modified file 'sql/sql_table.h'
>> --- a/sql/sql_table.h 2011-03-09 20:54:55 +0000
>> +++ b/sql/sql_table.h 2011-05-31 22:05:19 +0000
>> @@ -217,6 +217,8 @@ void release_ddl_log();
>> void execute_ddl_log_recovery();
>> bool execute_ddl_log_entry(THD *thd, uint first_entry);
>> bool check_duplicate_warning(THD *thd, char *msg, ulong length);
>> +int check_if_rpl_table(size_t db_len, const char *db, size_t table_name_len,
>> + const char *table_name);
>>
>> /*
>> These prototypes where under INNODB_COMPATIBILITY_HOOKS.
>>
>>
>>
>>
>>
>