Commit 19420d46 authored by marko's avatar marko

branches/zip: Merge revisions 968:1009 from trunk.

parent ac05c89b
This diff is collapsed.
......@@ -114,6 +114,7 @@ buf_flush_ready_for_replace(
{
#ifdef UNIV_SYNC_DEBUG
ut_ad(mutex_own(&(buf_pool->mutex)));
ut_ad(mutex_own(&block->mutex));
#endif /* UNIV_SYNC_DEBUG */
if (block->state != BUF_BLOCK_FILE_PAGE) {
ut_print_timestamp(stderr);
......@@ -149,6 +150,7 @@ buf_flush_ready_for_flush(
{
#ifdef UNIV_SYNC_DEBUG
ut_ad(mutex_own(&(buf_pool->mutex)));
ut_ad(mutex_own(&(block->mutex)));
#endif /* UNIV_SYNC_DEBUG */
ut_a(block->state == BUF_BLOCK_FILE_PAGE);
......@@ -635,8 +637,15 @@ buf_flush_try_page(
ut_a(!block || block->state == BUF_BLOCK_FILE_PAGE);
if (!block) {
mutex_exit(&(buf_pool->mutex));
return(0);
}
mutex_enter(&block->mutex);
if (flush_type == BUF_FLUSH_LIST
&& block && buf_flush_ready_for_flush(block, flush_type)) {
&& buf_flush_ready_for_flush(block, flush_type)) {
block->io_fix = BUF_IO_WRITE;
......@@ -661,6 +670,7 @@ buf_flush_try_page(
locked = TRUE;
}
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
if (!locked) {
......@@ -681,7 +691,7 @@ buf_flush_try_page(
return(1);
} else if (flush_type == BUF_FLUSH_LRU && block
} else if (flush_type == BUF_FLUSH_LRU
&& buf_flush_ready_for_flush(block, flush_type)) {
/* VERY IMPORTANT:
......@@ -709,13 +719,14 @@ buf_flush_try_page(
buf_pool mutex: this ensures that the latch is acquired
immediately. */
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
buf_flush_write_block_low(block);
return(1);
} else if (flush_type == BUF_FLUSH_SINGLE_PAGE && block
} else if (flush_type == BUF_FLUSH_SINGLE_PAGE
&& buf_flush_ready_for_flush(block, flush_type)) {
block->io_fix = BUF_IO_WRITE;
......@@ -729,6 +740,7 @@ buf_flush_try_page(
buf_pool->n_flush[flush_type]++;
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
rw_lock_s_lock_gen(&(block->lock), BUF_IO_WRITE);
......@@ -746,11 +758,12 @@ buf_flush_try_page(
buf_flush_write_block_low(block);
return(1);
} else {
mutex_exit(&(buf_pool->mutex));
return(0);
}
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
return(0);
}
/***************************************************************
......@@ -795,34 +808,48 @@ buf_flush_try_neighbors(
block = buf_page_hash_get(space, i);
ut_a(!block || block->state == BUF_BLOCK_FILE_PAGE);
if (block && flush_type == BUF_FLUSH_LRU && i != offset
&& !block->old) {
if (!block) {
continue;
} else if (flush_type == BUF_FLUSH_LRU && i != offset
&& !block->old) {
/* We avoid flushing 'non-old' blocks in an LRU flush,
because the flushed blocks are soon freed */
continue;
}
} else {
if (block && buf_flush_ready_for_flush(block, flush_type)
&& (i == offset || block->buf_fix_count == 0)) {
/* We only try to flush those neighbors != offset
where the buf fix count is zero, as we then know that
we probably can latch the page without a semaphore
wait. Semaphore waits are expensive because we must
flush the doublewrite buffer before we start
waiting. */
mutex_enter(&block->mutex);
mutex_exit(&(buf_pool->mutex));
if (buf_flush_ready_for_flush(block, flush_type)
&& (i == offset || block->buf_fix_count == 0)) {
/* We only try to flush those
neighbors != offset where the buf fix count is
zero, as we then know that we probably can
latch the page without a semaphore wait.
Semaphore waits are expensive because we must
flush the doublewrite buffer before we start
waiting. */
/* Note: as we release the buf_pool mutex above, in
buf_flush_try_page we cannot be sure the page is still
in a flushable state: therefore we check it again
inside that function. */
mutex_exit(&block->mutex);
count += buf_flush_try_page(space, i, flush_type);
mutex_exit(&(buf_pool->mutex));
mutex_enter(&(buf_pool->mutex));
/* Note: as we release the buf_pool mutex
above, in buf_flush_try_page we cannot be sure
the page is still in a flushable state:
therefore we check it again inside that
function. */
count += buf_flush_try_page(space, i,
flush_type);
mutex_enter(&(buf_pool->mutex));
} else {
mutex_exit(&block->mutex);
}
}
}
......@@ -918,12 +945,15 @@ buf_flush_batch(
while ((block != NULL) && !found) {
ut_a(block->state == BUF_BLOCK_FILE_PAGE);
mutex_enter(&block->mutex);
if (buf_flush_ready_for_flush(block, flush_type)) {
found = TRUE;
space = block->space;
offset = block->offset;
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
old_page_count = page_count;
......@@ -940,10 +970,14 @@ buf_flush_batch(
} else if (flush_type == BUF_FLUSH_LRU) {
mutex_exit(&block->mutex);
block = UT_LIST_GET_PREV(LRU, block);
} else {
ut_ad(flush_type == BUF_FLUSH_LIST);
mutex_exit(&block->mutex);
block = UT_LIST_GET_PREV(flush_list, block);
}
}
......@@ -1026,10 +1060,14 @@ buf_flush_LRU_recommendation(void)
+ BUF_FLUSH_EXTRA_MARGIN)
&& (distance < BUF_LRU_FREE_SEARCH_LEN)) {
mutex_enter(&block->mutex);
if (buf_flush_ready_for_replace(block)) {
n_replaceable++;
}
mutex_exit(&block->mutex);
distance++;
block = UT_LIST_GET_PREV(LRU, block);
......
......@@ -86,6 +86,11 @@ buf_LRU_invalidate_tablespace(
block = UT_LIST_GET_LAST(buf_pool->LRU);
while (block != NULL) {
buf_block_t* prev_block;
mutex_enter(&block->mutex);
prev_block = UT_LIST_GET_PREV(LRU, block);
ut_a(block->state == BUF_BLOCK_FILE_PAGE);
if (block->space == id
......@@ -112,6 +117,8 @@ buf_LRU_invalidate_tablespace(
if (block->is_hashed) {
page_no = block->offset;
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
/* Note that the following call will acquire
......@@ -138,7 +145,8 @@ buf_LRU_invalidate_tablespace(
buf_LRU_block_free_hashed_page(block);
}
next_page:
block = UT_LIST_GET_PREV(LRU, block);
mutex_exit(&block->mutex);
block = prev_block;
}
mutex_exit(&(buf_pool->mutex));
......@@ -211,6 +219,9 @@ buf_LRU_search_and_free_block(
while (block != NULL) {
ut_a(block->in_LRU_list);
mutex_enter(&block->mutex);
if (buf_flush_ready_for_replace(block)) {
#ifdef UNIV_DEBUG
......@@ -226,20 +237,27 @@ buf_LRU_search_and_free_block(
buf_LRU_block_remove_hashed_page(block);
mutex_exit(&(buf_pool->mutex));
mutex_exit(&block->mutex);
/* Remove possible adaptive hash index on the page */
btr_search_drop_page_hash_index(block);
mutex_enter(&(buf_pool->mutex));
ut_a(block->buf_fix_count == 0);
mutex_enter(&(buf_pool->mutex));
mutex_enter(&block->mutex);
buf_LRU_block_free_hashed_page(block);
freed = TRUE;
mutex_exit(&block->mutex);
break;
}
mutex_exit(&block->mutex);
block = UT_LIST_GET_PREV(LRU, block);
distance++;
......@@ -428,9 +446,13 @@ buf_LRU_get_free_block(
}
}
mutex_enter(&block->mutex);
block->state = BUF_BLOCK_READY_FOR_USE;
UNIV_MEM_VALID(block->frame, UNIV_PAGE_SIZE);
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
if (started_monitor) {
......@@ -816,6 +838,7 @@ buf_LRU_block_free_non_file_page(
{
#ifdef UNIV_SYNC_DEBUG
ut_ad(mutex_own(&(buf_pool->mutex)));
ut_ad(mutex_own(&block->mutex));
#endif /* UNIV_SYNC_DEBUG */
ut_ad(block);
......@@ -852,6 +875,7 @@ buf_LRU_block_remove_hashed_page(
const buf_block_t* hashed_block;
#ifdef UNIV_SYNC_DEBUG
ut_ad(mutex_own(&(buf_pool->mutex)));
ut_ad(mutex_own(&block->mutex));
#endif /* UNIV_SYNC_DEBUG */
ut_ad(block);
......@@ -911,6 +935,7 @@ buf_LRU_block_free_hashed_page(
{
#ifdef UNIV_SYNC_DEBUG
ut_ad(mutex_own(&(buf_pool->mutex)));
ut_ad(mutex_own(&block->mutex));
#endif /* UNIV_SYNC_DEBUG */
ut_a(block->state == BUF_BLOCK_REMOVE_HASH);
......
......@@ -4582,29 +4582,47 @@ fil_flush_file_spaces(
{
fil_system_t* system = fil_system;
fil_space_t* space;
ulint* space_ids;
ulint n_space_ids;
ulint i;
mutex_enter(&(system->mutex));
space = UT_LIST_GET_FIRST(system->unflushed_spaces);
n_space_ids = UT_LIST_GET_LEN(system->unflushed_spaces);
if (n_space_ids == 0) {
while (space) {
if (space->purpose == purpose && !space->is_being_deleted) {
mutex_exit(&system->mutex);
return;
}
space->n_pending_flushes++; /* prevent dropping of
the space while we are
flushing */
mutex_exit(&(system->mutex));
/* Assemble a list of space ids to flush. Previously, we
traversed system->unflushed_spaces and called UT_LIST_GET_NEXT()
on a space that was just removed from the list by fil_flush().
Thus, the space could be dropped and the memory overwritten. */
space_ids = mem_alloc(n_space_ids * sizeof *space_ids);
fil_flush(space->id);
n_space_ids = 0;
mutex_enter(&(system->mutex));
for (space = UT_LIST_GET_FIRST(system->unflushed_spaces);
space;
space = UT_LIST_GET_NEXT(unflushed_spaces, space)) {
space->n_pending_flushes--;
if (space->purpose == purpose && !space->is_being_deleted) {
space_ids[n_space_ids++] = space->id;
}
space = UT_LIST_GET_NEXT(unflushed_spaces, space);
}
mutex_exit(&(system->mutex));
mutex_exit(&system->mutex);
/* Flush the spaces. It will not hurt to call fil_flush() on
a non-existing space id. */
for (i = 0; i < n_space_ids; i++) {
fil_flush(space_ids[i]);
}
mem_free(space_ids);
}
/**********************************************************************
......
......@@ -4433,7 +4433,7 @@ ha_innobase::rnd_pos(
}
if (error) {
DBUG_PRINT("error", ("Got error: %ld", error));
DBUG_PRINT("error", ("Got error: %d", error));
DBUG_RETURN(error);
}
......@@ -4443,7 +4443,7 @@ ha_innobase::rnd_pos(
error = index_read(buf, pos, ref_length, HA_READ_KEY_EXACT);
if (error) {
DBUG_PRINT("error", ("Got error: %ld", error));
DBUG_PRINT("error", ("Got error: %d", error));
}
change_active_index(keynr);
......@@ -5482,7 +5482,7 @@ ha_innobase::read_time(
Returns statistics information of the table to the MySQL interpreter,
in various fields of the handle object. */
void
int
ha_innobase::info(
/*==============*/
uint flag) /* in: what information MySQL requests */
......@@ -5505,7 +5505,7 @@ ha_innobase::info(
if (srv_force_recovery >= SRV_FORCE_NO_IBUF_MERGE) {
DBUG_VOID_RETURN;
DBUG_RETURN(HA_ERR_CRASHED);
}
/* We do not know if MySQL can call this function before calling
......@@ -5697,7 +5697,7 @@ ha_innobase::info(
prebuilt->trx->op_info = (char*)"";
DBUG_VOID_RETURN;
DBUG_RETURN(0);
}
/**************************************************************************
......
......@@ -143,7 +143,7 @@ class ha_innobase: public handler
int rnd_pos(byte * buf, byte *pos);
void position(const byte *record);
void info(uint);
int info(uint);
int analyze(THD* thd,HA_CHECK_OPT* check_opt);
int optimize(THD* thd,HA_CHECK_OPT* check_opt);
int discard_or_import_tablespace(my_bool discard);
......
......@@ -663,7 +663,10 @@ struct buf_block_struct{
ulint magic_n; /* magic number to check */
ulint state; /* state of the control block:
BUF_BLOCK_NOT_USED, ... */
BUF_BLOCK_NOT_USED, ...; changing
this is only allowed when a thread
has BOTH the buffer pool mutex AND
block->mutex locked */
byte* frame; /* pointer to buffer frame which
is of size UNIV_PAGE_SIZE, and
aligned to an address divisible by
......@@ -672,8 +675,12 @@ struct buf_block_struct{
ulint offset; /* page number within the space */
ulint lock_hash_val; /* hashed value of the page address
in the record lock hash table */
mutex_t* lock_mutex; /* mutex protecting the chain in the
record lock hash table */
mutex_t mutex; /* mutex protecting this block:
state (also protected by the buffer
pool mutex), io_fix, buf_fix_count,
and accessed; we introduce this new
mutex in InnoDB-5.1 to relieve
contention on the buffer pool mutex */
rw_lock_t lock; /* read-write lock of the buffer
frame */
buf_block_t* hash; /* node used in chaining to the page
......@@ -725,20 +732,27 @@ struct buf_block_struct{
in heuristic algorithms, because of
the possibility of a wrap-around! */
ulint freed_page_clock;/* the value of freed_page_clock
buffer pool when this block was
last time put to the head of the
LRU list */
of the buffer pool when this block was
the last time put to the head of the
LRU list; a thread is allowed to
read this for heuristic purposes
without holding any mutex or latch */
ibool old; /* TRUE if the block is in the old
blocks in the LRU list */
ibool accessed; /* TRUE if the page has been accessed
while in the buffer pool: read-ahead
may read in pages which have not been
accessed yet */
accessed yet; this is protected by
block->mutex; a thread is allowed to
read this for heuristic purposes
without holding any mutex or latch */
ulint buf_fix_count; /* count of how manyfold this block
is currently bufferfixed */
is currently bufferfixed; this is
protected by block->mutex */
ulint io_fix; /* if a read is pending to the frame,
io_fix is BUF_IO_READ, in the case
of a write BUF_IO_WRITE, otherwise 0 */
of a write BUF_IO_WRITE, otherwise 0;
this is protected by block->mutex */
/* 4. Optimistic search field */
dulint modify_clock; /* this clock is incremented every
......@@ -872,7 +886,9 @@ struct buf_pool_struct{
number of buffer blocks removed from
the end of the LRU list; NOTE that
this counter may wrap around at 4
billion! */
billion! A thread is allowed to
read this for heuristic purposes
without holding any mutex or latch */
ulint LRU_flush_ended;/* when an LRU flush ends for a page,
this is incremented by one; this is
set to zero when a buffer block is
......
......@@ -285,12 +285,16 @@ buf_block_free(
/*===========*/
buf_block_t* block) /* in, own: block to be freed */
{
ut_a(block->state != BUF_BLOCK_FILE_PAGE);
mutex_enter(&(buf_pool->mutex));
mutex_enter(&block->mutex);
ut_a(block->state != BUF_BLOCK_FILE_PAGE);
buf_LRU_block_free_non_file_page(block);
mutex_exit(&block->mutex);
mutex_exit(&(buf_pool->mutex));
}
......@@ -427,7 +431,7 @@ buf_block_buf_fix_inc_debug(
ret = rw_lock_s_lock_func_nowait(&(block->debug_latch), file, line);
ut_a(ret);
ut_a(mutex_own(&block->mutex));
block->buf_fix_count++;
}
#else /* UNIV_SYNC_DEBUG */
......@@ -531,22 +535,23 @@ buf_page_release(
{
ut_ad(block);
mutex_enter_fast(&(buf_pool->mutex));
ut_a(block->state == BUF_BLOCK_FILE_PAGE);
ut_a(block->buf_fix_count > 0);
if (rw_latch == RW_X_LATCH && mtr->modifications) {
mutex_enter(&buf_pool->mutex);
buf_flush_note_modification(block, mtr);
mutex_exit(&buf_pool->mutex);
}
mutex_enter(&block->mutex);
#ifdef UNIV_SYNC_DEBUG
rw_lock_s_unlock(&(block->debug_latch));
#endif
block->buf_fix_count--;
mutex_exit(&(buf_pool->mutex));
mutex_exit(&block->mutex);
if (rw_latch == RW_S_LATCH) {
rw_lock_s_unlock(&(block->lock));
......
......@@ -80,6 +80,7 @@ memory is read outside the allocated blocks. */
#define UNIV_DEBUG_VALGRIND
#define UNIV_DEBUG_PRINT
#define UNIV_DEBUG
#define UNIV_LIST_DEBUG
#define UNIV_MEM_DEBUG
#define UNIV_IBUF_DEBUG
#define UNIV_SYNC_DEBUG
......
......@@ -123,27 +123,36 @@ name, NODE1 and NODE2 are pointers to nodes. */
}\
}\
/* Invalidate the pointers in a list node. */
#ifdef UNIV_LIST_DEBUG
# define UT_LIST_REMOVE_CLEAR(NAME, N) \
((N)->NAME.prev = (N)->NAME.next = (void*) -1)
#else
# define UT_LIST_REMOVE_CLEAR(NAME, N) while (0)
#endif
/***********************************************************************
Removes a node from a two-way linked list. BASE has to be the base node
(not a pointer to it). N has to be the pointer to the node to be removed
from the list. NAME is the list name. */
#define UT_LIST_REMOVE(NAME, BASE, N)\
{\
ut_ad(N);\
ut_a((BASE).count > 0);\
((BASE).count)--;\
if (((N)->NAME).next != NULL) {\
((((N)->NAME).next)->NAME).prev = ((N)->NAME).prev;\
} else {\
(BASE).end = ((N)->NAME).prev;\
}\
if (((N)->NAME).prev != NULL) {\
((((N)->NAME).prev)->NAME).next = ((N)->NAME).next;\
} else {\
(BASE).start = ((N)->NAME).next;\
}\
}\
#define UT_LIST_REMOVE(NAME, BASE, N) \
do { \
ut_ad(N); \
ut_a((BASE).count > 0); \
((BASE).count)--; \
if (((N)->NAME).next != NULL) { \
((((N)->NAME).next)->NAME).prev = ((N)->NAME).prev; \
} else { \
(BASE).end = ((N)->NAME).prev; \
} \
if (((N)->NAME).prev != NULL) { \
((((N)->NAME).prev)->NAME).next = ((N)->NAME).next; \
} else { \
(BASE).start = ((N)->NAME).next; \
} \
UT_LIST_REMOVE_CLEAR(NAME, N); \
} while (0)
/************************************************************************
Gets the next node in a two-way list. NAME is the name of the list
......
......@@ -4548,10 +4548,6 @@ lock_print_info_all_transactions(
trx->read_view->up_limit_id));
}
fprintf(file,
"Trx has approximately %lu row locks\n",
(ulong) lock_number_of_rows_locked(trx));
if (trx->que_state == TRX_QUE_LOCK_WAIT) {
fprintf(file,
"------- TRX HAS BEEN WAITING %lu SEC"
......
SET SESSION STORAGE_ENGINE = InnoDB;
drop table if exists t1,t2,t1m,t1i,t2m,t2i,t4;
create table t1(f1 varchar(800) binary not null, key(f1))
character set utf8 collate utf8_general_ci;
Warnings:
Warning 1071 Specified key was too long; max key length is 765 bytes
insert into t1 values('aaa');
drop table t1;
create table t1 (
c_id int(11) not null default '0',
org_id int(11) default null,
......@@ -111,6 +105,14 @@ SELECT `id1` FROM `t1` WHERE `id1` NOT IN (SELECT `id1` FROM `t2` WHERE `id2` =
id1
2
DROP TABLE t1, t2;
create table t1 (c1 int) engine=innodb;
handler t1 open;
handler t1 read first;
c1
Before and after comparison
0
drop table t1;
End of 4.1 tests
create table t1m (a int) engine = MEMORY;
create table t1i (a int);
create table t2m (a int) engine = MEMORY;
......@@ -248,6 +250,22 @@ b
c
d
drop table t1,t4;
DROP TABLE IF EXISTS t2, t1;
CREATE TABLE t1 (i INT NOT NULL PRIMARY KEY) ENGINE= InnoDB;
CREATE TABLE t2 (
i INT NOT NULL,
FOREIGN KEY (i) REFERENCES t1 (i) ON DELETE NO ACTION
) ENGINE= InnoDB;
INSERT INTO t1 VALUES (1);
INSERT INTO t2 VALUES (1);
DELETE IGNORE FROM t1 WHERE i = 1;
Warnings:
Error 1451 Cannot delete or update a parent row: a foreign key constraint fails (`test`.`t2`, CONSTRAINT `t2_ibfk_1` FOREIGN KEY (`i`) REFERENCES `t1` (`i`) ON DELETE NO ACTION)
SELECT * FROM t1, t2;
i i
1 1
DROP TABLE t2, t1;
End of 4.1 tests.
create table t1 (
a varchar(30), b varchar(30), primary key(a), key(b)
);
......@@ -369,6 +387,23 @@ Warnings:
Warning 1071 Specified key was too long; max key length is 765 bytes
insert into t1 values('aaa');
drop table t1;
CREATE TABLE t1 (a INT PRIMARY KEY, b INT, c FLOAT, KEY b(b)) ENGINE = INNODB;
INSERT INTO t1 VALUES ( 1 , 1 , 1);
INSERT INTO t1 SELECT a + 1 , MOD(a + 1 , 20), 1 FROM t1;
INSERT INTO t1 SELECT a + 2 , MOD(a + 2 , 20), 1 FROM t1;
INSERT INTO t1 SELECT a + 4 , MOD(a + 4 , 20), 1 FROM t1;
INSERT INTO t1 SELECT a + 8 , MOD(a + 8 , 20), 1 FROM t1;
INSERT INTO t1 SELECT a + 16, MOD(a + 16, 20), 1 FROM t1;
INSERT INTO t1 SELECT a + 32, MOD(a + 32, 20), 1 FROM t1;
INSERT INTO t1 SELECT a + 64, MOD(a + 64, 20), 1 FROM t1;
EXPLAIN SELECT b, SUM(c) FROM t1 GROUP BY b;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 index NULL b 5 NULL 128
EXPLAIN SELECT SQL_BIG_RESULT b, SUM(c) FROM t1 GROUP BY b;
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE t1 ALL NULL NULL NULL NULL 128 Using filesort
DROP TABLE t1;
End of 5.0 tests
CREATE TABLE `t2` (
`k` int(11) NOT NULL auto_increment,
`a` int(11) default NULL,
......@@ -437,3 +472,4 @@ k a c
11 15 1
12 20 1
drop table t2;
End of 5.1 tests
......@@ -68,4 +68,5 @@ MYSQL_PLUGIN_ACTIONS(innobase, [
storage/innobase/handler/Makefile
storage/innobase/usr/Makefile)
])
MYSQL_PLUGIN_DEPENDS_ON_MYSQL_INTERNALS(innobase, [handler/ha_innodb.cc])
......@@ -1761,11 +1761,10 @@ trx_print(
|| mem_heap_get_size(trx->lock_heap) > 400) {
newline = TRUE;
fprintf(f, "%lu lock struct(s), heap size %lu",
fprintf(f, "%lu lock struct(s), heap size %lu,"
" %lu row lock(s)",
(ulong) UT_LIST_GET_LEN(trx->trx_locks),
(ulong) mem_heap_get_size(trx->lock_heap));
fprintf(f, "%lu row lock(s)",
(ulong) mem_heap_get_size(trx->lock_heap),
(ulong) lock_number_of_rows_locked(trx));
}
......
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