List:Internals« Previous MessageNext Message »
From:Michael Widenius Date:August 27 2009 9:35pm
Subject:re: QC patch for review
View as plain text  
Hi!

This is regarding a bug fix for query cache:
- Bug#39249  Maria: query cache returns out-of-date results
- Bug #41098 Query Cache returns wrong result with concurrent insert

>>>>> "s" == Oleksandr Byelkin <Oleksandr> writes:

s> Hi!
s> Here it is QC patch for inserts (as we discussed) and also there is DBUG
s> problem workaround in the test.

s> +++ sql/handler.h	2009-06-11 11:25:58 +0000
s> @@ -247,6 +247,11 @@
s>  #define HA_CACHE_TBL_NOCACHE     1
s>  #define HA_CACHE_TBL_ASKTRANSACT 2
s>  #define HA_CACHE_TBL_TRANSACT    4
s> +/**
s> +  Non transactional table but insert results visible for other threads
s> +  only on unlock
s> +*/
s> +#define HA_CACHE_TBL_NTRNS_INS2LOCK 8

The above is true for all non transactional engines. To clarify, there
are two types of non transactional engines when it comes to insert:

1) The table is locked at start of insert statement and can't be accessed by
   other threads until unlock.
2) The table is locked at start of insert statement, but other
   threads can read only the original rows until unlock, after which
   all rows are visible (myisam/maria with concurrent insert on)
 
In  both cases, the new rows are visible after unlock, in which case
we shouldn't need the above flag.

As we discussed in Majorca, I think the logic for this patch is wrong.
The current patch works (as I can see) on the following idea:

- mark non-transacitonal tables that are part of an insert
- later in sql_base.cc::close_thread_table() call
  query_cache_invalidate4() to invalidate the table if there was a
  possible 'concurrent insert'.

The reason the above doesn't work is that another thread may
have done a lock on the table before the unlock happens in the
concurrent-inserted thread and then call 'query_cache_store_query()'
after the invalidate is done. In this case the other thread has
and old view of the table and anything it stores in the cache will
be 'old'.

The new code only makes wrong things 'less unlikely' to happen; It
doesn't remove the original bug.

The reason for the problem with concurrent insert problem in the query
cache are two:

- The query cache should get invalidated at once when table changes
  state (ie, when the new table content is visible for another
  thread).  This should be done under a mutex to ensure that there is
  no race condition for this table.
- When validating of select with a table can be put into the query
  cache we should ensure that it's content is of the latest version.

With the two above axioms in place, the query cache will be safe for
concurrent inserts.  There are some other benefits of changing to use
this model:

- We can safely use locked tables with the query cache.
  - We can put the query into the table cache even under lock tables,
    as long as all the tables are marked as 'cacheable'.
  - We can use the data from the query cache, as long as all locked
    tables are marked as 'cacheable'.

- We could (later) merge the code of transactional and not
  transactional tables as they act the same.

- We can provide caching for versioned tables (like Maria) more easily.

Here is a last comment about the problem with this patch:

s> === modified file 'sql/sql_base.cc'
s> --- sql/sql_base.cc	2009-05-19 09:28:05 +0000
s> +++ sql/sql_base.cc	2009-06-11 11:25:58 +0000
s> @@ -1373,6 +1373,15 @@ bool close_thread_table(THD *thd, TABLE 
s>                          table->s->table_name.str, (long) table));
 
s>    *table_ptr=table->next;
s> +
s> +  /* Invalidate if it has mark about changing in insert
s> +    (not all tables has such marks */

Fix comment style and add missing brace

+  if (table->changed_in_insert)
+  {
+    table->changed_in_insert= FALSE;
+    query_cache_invalidate4(thd, table, FALSE, FALSE);
+  }

I have some problems with the above code:

- The real problem is that if someone accesses the table between
  the invalidate() and unlock, they see the old version of the table
  and not the real rows.
  The above change adds an extra invalidate in case of concurrent
  inserts.  However, another thread may already have done the
  following before this call:

  - Do a query just after invalidate, in which case the table is seen
    as 'old'
  - Do another query after unlock, in which case table is seen as
    'new'.

- Another problem is that another thread that did a lock table
  before the invalidate, it will still see the old data from the table.
  If this thread does a query of the table and it goes into the query
  cache, then the query cache will be filled with wrong data.
  (Note that even if we don't put tables in the cache under lock
   tables, the problem is still the same for tables without lock
   tables. It's just easier to simulate the problem if we test things
  with lock tables)
 
- A future problem is that for nontransactional tables that could do
  concurrent deletes/updates the above wouldn't work.

So this code only makes the problem less likely, it doesn't fix it.

----------------------

Add to MyISAM a function:

virtual my_bool register_query_cache_table(...)
{
  /* check if this thread is seeing all rows in the file */
  if (file->s->state.data_file_length !=
      file->state->data_file_length)
  {
    /*
       Some thread (or even this thread) has inserted more rows into the
       table since it was locked; Don't use the query cache.
    */      
    return 0;
  }
 return 1;
}


We also need to invalidate tables when they change. This should be
done by adding a callback in mi_locking.c:mi_update_status()
to invalidate the query cache for any entries for this table.
(This only needs to be done inside 'if (info->state == &info->save_state)'


In Maira we need to to the same changes in _ma_update_status().

In Maria, we can also decide to handle transactional/versioned tables with the 
query cache:

virtual my_bool register_query_cache_table(....)
{

....

  if (share->now_transactional && share->have_versioning)
    return (file->trn != file->s->state.last_change_trn);
...
}


For the above to work, we need to set last_change_trn in
_ma_trnman_end_trans_hook() when we store the state for a changed
table (at same place where we update 'records' and 'checksum')

Regards,
Monty

PS: Don't forget to also fix the MERGE tables and check that the Maria
    bug is truly fixed.
Thread
re: QC patch for reviewMichael Widenius27 Aug