2

I am getting the error MysqlError: Duplicate entry '1-5' for key 'PRIMARY' as shown below in the code. It only happened once (that I could detect, but it was random) and I couldn't find a cause (New Relic reported), but I cannot reproduce and I don't have much more information except the line number and the error given. The schema and code is below.

num_rows() is somehow returning a value that is not 1 even though it shouldn't. If someone can give some insight on how to debug or fix that would be helpful.

Here is my schema for location_items:

CREATE TABLE `phppos_location_items` (
  `location_id` int(11) NOT NULL,
  `item_id` int(11) NOT NULL,
  `location` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `cost_price` decimal(23,10) DEFAULT NULL,
  `unit_price` decimal(23,10) DEFAULT NULL,
  `promo_price` decimal(23,10) DEFAULT NULL,
  `start_date` date DEFAULT NULL,
  `end_date` date DEFAULT NULL,
  `quantity` decimal(23,10) DEFAULT '0.0000000000',
  `reorder_level` decimal(23,10) DEFAULT NULL,
  `override_default_tax` int(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`location_id`,`item_id`),
  KEY `phppos_location_items_ibfk_2` (`item_id`),
  CONSTRAINT `phppos_location_items_ibfk_1` FOREIGN KEY (`location_id`) REFERENCES `phppos_locations` (`location_id`),
  CONSTRAINT `phppos_location_items_ibfk_2` FOREIGN KEY (`item_id`) REFERENCES `phppos_items` (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |

And the code:

//Lock tables involved in sale transaction so we do not have deadlock
$this->db->query('LOCK TABLES '.$this->db->dbprefix('customers').' WRITE, '.$this->db->dbprefix('receivings').' WRITE, 
'.$this->db->dbprefix('store_accounts').' WRITE, '.$this->db->dbprefix('receivings_items').' WRITE, 
'.$this->db->dbprefix('giftcards').' WRITE, '.$this->db->dbprefix('location_items').' WRITE, 
'.$this->db->dbprefix('inventory').' WRITE, 
'.$this->db->dbprefix('people').' READ,'.$this->db->dbprefix('items').' WRITE
,'.$this->db->dbprefix('employees_locations').' READ,'.$this->db->dbprefix('locations').' READ, '.$this->db->dbprefix('items_tier_prices').' READ
, '.$this->db->dbprefix('location_items_tier_prices').' READ, '.$this->db->dbprefix('items_taxes').' READ, '.$this->db->dbprefix('item_kits').' READ
, '.$this->db->dbprefix('location_item_kits').' READ, '.$this->db->dbprefix('item_kit_items').' READ, '.$this->db->dbprefix('employees').' READ , '.$this->db->dbprefix('item_kits_tier_prices').' READ
, '.$this->db->dbprefix('location_item_kits_tier_prices').' READ, '.$this->db->dbprefix('suppliers').' READ, '.$this->db->dbprefix('location_items_taxes').' READ
, '.$this->db->dbprefix('location_item_kits_taxes'). ' READ, '.$this->db->dbprefix('item_kits_taxes'). ' READ');


    // other code for inserting data into other tables that are not relevant.

    foreach($items as $line=>$item)
    {
        $cur_item_location_info->quantity = $cur_item_location_info->quantity !== NULL ? $cur_item_location_info->quantity : 0;
        $quantity_data=array(
            'quantity'=>$cur_item_location_info->quantity + $item['quantity'],
            'location_id'=>$this->Employee->get_logged_in_employee_current_location_id(),
            'item_id'=>$item['item_id']

        );
            $this->Item_location->save($quantity_data,$item['item_id']);
    }
    // other code for inserting data into other tables that are not relevant.

    $this->db->query('UNLOCK TABLES');


class Item_location extends CI_Model
{
    function exists($item_id,$location=false)
    {
        if(!$location)
        {
            $location= $this->Employee->get_logged_in_employee_current_location_id();
        }
        $this->db->from('location_items');
        $this->db->where('item_id',$item_id);
        $this->db->where('location_id',$location);
        $query = $this->db->get();

        return ($query->num_rows()==1);
    }


    function save($item_location_data,$item_id=-1,$location_id=false)
    {
        if(!$location_id)
        {
            $location_id= $this->Employee->get_logged_in_employee_current_location_id();
        }

        if (!$this->exists($item_id,$location_id))
        {
            $item_location_data['item_id'] = $item_id;
            $item_location_data['location_id'] = $location_id;

            //MysqlError: Duplicate entry '1-5' for key 'PRIMARY'
            return $this->db->insert('location_items',$item_location_data);
        }

        $this->db->where('item_id',$item_id);
        $this->db->where('location_id',$location_id);
        return $this->db->update('location_items',$item_location_data);

    }
}

function get_logged_in_employee_current_location_id()
    {
        if($this->is_logged_in())
        {
            //If we have a location in the session
            if ($this->session->userdata('employee_current_location_id')!==FALSE)
            {
                return $this->session->userdata('employee_current_location_id');
            }

            //Return the first location user is authenticated for
            return current($this->get_authenticated_location_ids($this->session->userdata('person_id')));
        }

        return FALSE;
    }
Chris Muench
  • 17,444
  • 70
  • 209
  • 362
  • 3
    If _you_ can not reproduce it, how do you expect _we_ will be able to do it? – Alma Do Jul 16 '14 at 13:38
  • Maybe you will see something in the code that I am not testing. @AlmaDo – Chris Muench Jul 16 '14 at 13:39
  • How are you generating `$item_id`? – Jim Jul 16 '14 at 13:40
  • @Jim $item_id is passed in from a function – Chris Muench Jul 16 '14 at 13:41
  • You're inserting an item with the `location_id` of `1` and the `item_id` of `5` when one already exists, primary keys are unique. Judging by the way you're getting to to claim the row isn't unique makes me think maybe your exists check is wrong. Could you try checking the actual value `$query->num_rows()` using var_dump? – scragar Jul 21 '14 at 12:07
  • It happens randomly so I am thinking mysql(i)_num_rows could be returning FALSE if there was an error causing it to return false when it actually exists. I have added logging for this condition. – Chris Muench Jul 21 '14 at 15:15
  • 1
    It is misleading to say it only happened once if it happened randomly. Please consider clarifying your question. – smcjones Jul 21 '14 at 18:37
  • Can you add this function code `get_logged_in_employee_current_location_id()` ?? – Qarib Haider Jul 22 '14 at 06:40
  • @ChrisMuench you say there was only 1 call to the url in the log. some http server configs log failed calls into different files are you sure you checked all log files? – FuzzyTree Jul 23 '14 at 13:00
  • I am using apache. I check error log and access log on both servers (load balanced) – Chris Muench Jul 23 '14 at 13:25
  • your primary key is build of two foreign keys. How can you be sure this combination will always be unique? I cannot see that in the code you provided. – Thomas Köhne Jul 23 '14 at 17:02
  • @ThomasKöhne Thats why I have the `exists` method so I insert if they are not found and update if they are found. Do you see a problem with this? – Chris Muench Jul 23 '14 at 17:07

8 Answers8

5

It's not a good idea to check for existence prior to inserting data outside a transaction as this leaves open the possibility of data changing in the mean time. The fact that you've seen this error once but it isn't easily repeatable makes me wonder whether this might have happened.

Would suggest changing the code beneath the first if block in the save function to something that generates the following SQL instead: INSERT INTO location_items (item_id, location_id) VALUES ($item_id,$location_id) ON DUPLICATE KEY UPDATE

This covers the existence check and insert or update in a single atomic statement. (To take this any further and say how to actually implement it I'd need access to the db code.)

EDIT: Sorry, only just noticed the db code is CodeIgniter. Am new to this framework but the above method looks perfectly possible from a brief look here. Something like this:

$sql = "INSERT INTO location_items (item_id, location_id)"
    . " VALUES (?, ?)"
    . " ON DUPLICATE KEY UPDATE"; 
$this->db->query($sql, array($item_id, $location_id));

(If for some reason you prefer not to do this, another way to keep it atomic would be to wrap the statements within a transaction instead ($this->db->trans_start(); before the existence check and $this->db->trans_complete(); after the insert/update. But IMO this introduces unnecessary complexity - personally much prefer the first method.)

Steve Chambers
  • 37,270
  • 24
  • 156
  • 208
  • This is most likely what I will do to fix the problem in the future. CodeIgniter makes it difficult to do a statement such as this and I like the logic I currently have but I might have to change it to be atomic. I am hoping my logging finds the issue. Do you know if it is possible for 1 query to fail randomly in mysql within one connection? I am starting to think there was a small network blip. – Chris Muench Jul 23 '14 at 14:33
  • Please see edit to the post fleshing it out a bit. Can't say for sure but a random blip sounds unlikely - more likely to be a race condition / concurrency issue IMO. – Steve Chambers Jul 23 '14 at 20:36
  • It is hard to see how it would be a concurrency issue as I looked at the logs for both my web servers and for that database there were no requests near each other. My application gives every user a database that has the same schema and the server was under very light load and memory for web servers and mysql were below 50% and CPU was not high at all. Do you think it is possible that num_rows() returned FALSE for an odd reason? – Chris Muench Jul 23 '14 at 20:52
  • Also thanks for the tip for on duplicate key update. That should work well if I have to go that route. – Chris Muench Jul 23 '14 at 20:55
  • Is it possible that another database request could affect this user's database as a race condition? Every company has their own database with the same schema...but I wouldn't think they would interfere. I looked a the logs of this one user and there were no requests within a minute of the one that caused the issue. – Chris Muench Jul 24 '14 at 01:18
  • Two separate databases wouldn't interfere with each other but two users on the same database could. Did the error happen in a live system or during development? (Seems more likely to happen if for example a breakpoint were set or slowly stepping through the code while another process was running.) – Steve Chambers Jul 24 '14 at 12:44
  • It happened on a live machine. – Chris Muench Jul 24 '14 at 13:14
  • Can you tell if there were many users on the system at the time? – Steve Chambers Jul 24 '14 at 13:55
  • Based on the access logs for both web servers their appeared to be just one user for this subdomain/company. – Chris Muench Jul 24 '14 at 14:53
  • If you look at the whole system at peek traffic I get at 200 requests per minute which 2 servers can easily handle. I have 2 web servers and 1 database seder. Memory and CPU usage for all servers was minimal at the time. – Chris Muench Jul 24 '14 at 15:02
  • Hmmm well not sure then. Guess it's still possible with one user e.g. different sessions or a database update that was external to the system. Might be one of those where you can never be sure of the true cause but the suggested update in the post should at least stop it from happening in future. – Steve Chambers Jul 24 '14 at 18:38
1

Looks like a race condition. What likely happened is to roughly simultaneous calls to:

save($data,5);

both get to the exists check at the same time and see that there is no existing entry. Both then try to insert and the fastest gun wins.

Jim
  • 22,354
  • 6
  • 52
  • 80
  • I looked at a database backup and it is very unlikely that an entry didn't exist in location_items as it was there 7 days ago and the user doesn't have access to delete from that table in any way. Is there any other way you can think of where that would return false for some reason? Is it possible that there was a connection error for a brief second? – Chris Muench Jul 16 '14 at 13:58
  • I added more code that might be relevant. I am locking tables to prevent a deadlock. I am checking with my host to see if there was any sort of outage. I am trying to figure out how to track this down. – Chris Muench Jul 16 '14 at 16:13
  • I also checked the log and there was only one call to the url at that time. So that rules out a race condition. This one is really hard to debug. – Chris Muench Jul 16 '14 at 17:40
1

You are not going to get a solution so long as the following conditions exist:

  1. You cannot reproduce this problem yourself.
  2. You do not share your source code and database for someone else to attempt to replicate.

I am not asking you to share your full source code. Rather, I am saying this to temper your expectations.

That being said, duplicates can exist for numerous reasons. It would help your question if you provided your version, but I did find one reason that could be a cause: Memory too low - could be reproducible if you lower your memory or put a high strain on your system. If you've had a hard time reproducing it, memory could well be why as you may not be trying to simulate that.

Other things to consider:

  • You may be wasting your time trying to duplicate something that just will not be duplicated.
  • If you are concerned you will experience this issue again, you should really consider logging. That can help you to track down the query which caused the issue. I would advise that you not have logging in a production environment and only in development, because it will probably lead to performance penalties that could well be significant. If this is a one-off issue you may never see it again, but it doesn't hurt to be prepared and armed with more information if the issue appears again.

Ultimately, debugging requires the ability to reproduce the error. A bug is part of a computer program, which means there are certain situations and environments in which this will occur, which can be reproduced. When you have no idea how or why a bug was caused there is nowhere to work back from. It is helpful to look to auxiliary concerns as the potential source of your issue by exploring bug reports, etc. If that fails, implement tools like logging that give you more information. This is the only way you will be able to find the root cause of this issue, or get any more specific insight from the SO community on how to do so.

Community
  • 1
  • 1
smcjones
  • 5,490
  • 1
  • 23
  • 39
0

I suspect that the problem could be related with the cache of where statements. This suspect comes from this stackoverflow question.

Basically I think it could happen that:

- in one cycle this code is executed at the end of save method:
     $this->db->where('item_id',$item_id);
     $this->db->where('location_id',$location_id);
     return $this->db->update('location_items',$item_location_data);

  - in the subsequent cycle this code is executed in the exists method:
     $this->db->where('item_id',$item_id);
     $this->db->where('location_id',$location_id);
     return $this->db->update('location_items',$item_location_data);

When executing the "exists" code the cache may still contain the where clauses of the previous statement and the new one (different) will be added. This way the result will be empty and it seems that the row it is not in the table.

Try to use $this->db->flush_cache(); after the update in the save method.

Also try to use echo $this->db->last_query(); to see what is trying to do in the exists query.

Community
  • 1
  • 1
Diego Mazzaro
  • 2,734
  • 1
  • 20
  • 21
  • It looks like active record caching is disable be default and `get()` resets it according to docs http://ellislab.com/codeigniter/user-guide/database/active_record.html#caching ` Normally, when an Active Record call is completed, all stored information is reset for the next call. With caching, you can prevent this reset, and reuse information easily.` – Chris Muench Jul 20 '14 at 18:21
  • Ok, so maybe is not this. Only one thing.. there is any get executed between the update and the "exists" query. I have not been able to find in the documentation that also update resets the chaining of where also if I would say that it is the expected behavior. Anyway, since you are doing logging, I will re-recommend to use `$this->db->last_query()` to see what's going on. Good luck! – Diego Mazzaro Jul 20 '14 at 19:22
  • I have added the following log if `num_rows()===FALSE` (This is what I think is happening) `file_put_contents('../log/query_error.txt', $this->db->_error_number(). '-'.$this->db->_error_message(). '-'.$this->db->last_query(),FILE_APPEND);` – Chris Muench Jul 20 '14 at 19:44
0

may be ' phppos_location_items ' table is exist in past and a delete statement is executed over this table Delete from phppos_location_items; in this case primary key column not accept previous values if you truncate the table then all previous record will be removed

Ashutosh SIngh
  • 931
  • 3
  • 13
  • 26
0

Sorry its slightly long for a comment ..

What submits the form ? I assume its a button somewhere on a Page, When I have had a similar error its been due to a user double clicking on a button and 2 requests being sent, in a very close time to each other. This caused a check similar to yours to have this situatuion

Request 1     Check      Insert
Request 2           Check      Insert 

As Request 2 was the last request (because the second click took priority) the error was shown though the first request completed all of the work.

i use the code

$("form").submit(function() {
    $(this).submit(function() {
        return false;
    });
    return true;
});

from this question

How to prevent form from submitting multiple times from client side?

Community
  • 1
  • 1
exussum
  • 18,275
  • 8
  • 32
  • 65
  • Thanks for the response. I checked the logs and there was only one request for that time period and I do prevent double submission from client side. Regardless I am almost certain that the exists method should return true unless there was some sort of error. (Which is why I am adding logging). – Chris Muench Jul 22 '14 at 23:09
0

Try to use

$insert =  $this->db->insert('location_items',$item_location_data);
if($insert)
{
$this->db->reset();
//OR
try $this->db->_reset_write(); to flush all traces of the query
return $insert;
}
Sadikhasan
  • 18,365
  • 21
  • 80
  • 122
0

Solution 1:

Duplicate entry states that you have one more row which has a primary key same as that of some other previous row. You can ignore this by statement

INSERT IGNORE INTO ..(rest is same, just add ignore)..

Solution 2:

If you wanna overwrite previous row with new one then you need to follow this query:

INSERT INTO TABLE (f1,f2) VALUES ('f1','f2') ON DUPLICATE KEY UPDATE f1='f1',f2='f2'

Solution: 3

Change your primary key by following these queries:

Create new field for primary key:

ALTER TABLE tablename ADD new_primary_key BIGINT NOT NULL FIRST;

Drop existing primary key:

ALTER TABLE tablename DROP PRIMARY KEY

Now make the new field created earlier as primary key with auto increment

ALTER TABLE tablename MODIFY new_primary_key BIGINT AUTO_INCREMENT PRIMARY KEY

(this will not affect other queries in the code, you can keep them as it is and just add LIMIT 1 in select statements)

user1735921
  • 1,359
  • 1
  • 21
  • 46