I found out that this probably isn't concurrency problem as the method is recalled JUST WHEN I TRY TO UPDATE THE
sync.test.subject.b
'sseparated_chars
FIELD (at the end of the method). So I can't solve this with thread locking as the method actually waits for itself to be called again. I don't get it this is a totally bizarre behavior.
I found a weird behavior while updating computed fields. In this case codes are better than words:
Models:
from openerp import models, fields, api, _
class sync_test_subject_a(models.Model):
_name = "sync.test.subject.a"
name = fields.Char('Name')
sync_test_subject_a()
class sync_test_subject_b(models.Model):
_name = "sync.test.subject.b"
chars = fields.Char('Characters')
separated_chars = fields.Many2many('sync.test.subject.a',string='Separated Name', store=True, compute='_compute_separated_chars')
@api.depends('chars')
def _compute_separated_chars(self):
print "CHAR SEPARATION BEGIN"
sync_test_subject_a_pool = self.env['sync.test.subject.a']
print "SEPARATE CHARS"
# SEPARATE CHARS
characters = []
if self.chars:
for character in self.chars:
characters.append(character)
print "DELETE CURRENT CHARS"
# DELETE CURRENT MANY2MANY LINK
self.separated_chars.unlink()
print "DELETE EXISTING sync_test_subject_a THAT ARE RELATED TO CURRENT SELF"
# DELETE EXISTING sync_test_subject_a THAT ARE RELATED TO CURRENT SELF
deleted_separated_char_ids = []
for separated_char in self.separated_chars:
deleted_separated_char_ids.append(separated_char.sync_test_subject_a_id.id)
sync_test_subject_a_pool.browse(deleted_separated_char_ids).unlink()
print "INSERT NEW CHAR RECORDS"
#INSERT NEW CHAR RECORDS
separated_char_ids = []
for character in characters:
separated_char_ids.append(sync_test_subject_a_pool.create({'name':character}).id)
print "UPDATE self.separated_chars WITH CHAR IDS"
#UPDATE self.separated_chars WITH CHAR IDS
self.separated_chars = separated_char_ids
print "CHAR SEPARATION END"
sync_test_subject_b()
class sync_test_subject_c(models.Model):
_name = "sync.test.subject.c"
_inherit = "sync.test.subject.b"
name = fields.Char('Name')
@api.one
def action_set_char(self):
self.chars = self.name
sync_test_subject_c()
Views:
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- Top menu item -->
<menuitem name="Testing Module"
id="testing_module_menu"
sequence="1"/>
<menuitem id="sync_test_menu" name="Synchronization Test" parent="testing_module_menu" sequence="1"/>
<!--Expense Preset View-->
<record model="ir.ui.view" id="sync_test_subject_c_form_view">
<field name="name">sync.test.subject.c.form.view</field>
<field name="model">sync.test.subject.c</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Sync Test" version="7.0">
<header>
<div class="header_bar">
<button name="action_set_char" string="Set Name To Chars" type="object" class="oe_highlight"/>
</div>
</header>
<sheet>
<group>
<field string="Name" name="name" class="oe_inline"/>
<field string="Chars" name="chars" class="oe_inline"/>
<field string="Separated Chars" name="separated_chars" class="oe_inline"/>
</group>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="sync_test_subject_c_tree_view">
<field name="name">sync.test.subject.c.tree.view</field>
<field name="model">sync.test.subject.c</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Class">
<field string="Name" name="name"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="sync_test_subject_c_search">
<field name="name">sync.test.subject.c.search</field>
<field name="model">sync.test.subject.c</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Sync Test Search">
<field string="Name" name="name"/>
</search>
</field>
</record>
<record id="sync_test_subject_c_action" model="ir.actions.act_window">
<field name="name">Sync Test</field>
<field name="res_model">sync.test.subject.c</field>
<field name="view_type">form</field>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="view_id" eval="sync_test_subject_c_tree_view"/>
<field name="search_view_id" ref="sync_test_subject_c_search"/>
<field name="target">current</field>
<field name="help">Synchronization Test</field>
</record>
<menuitem action="sync_test_subject_c_action" icon="STOCK_JUSTIFY_FILL" sequence="1"
id="sync_test_subject_c_action_menu" parent="testing_module.sync_test_menu"
/>
</data>
</openerp>
There are 3 classes, sync.test.subject.a
which has many2many relation with sync.test.subject.b
which is inherited by sync.test.subject.c
.
sync.test.subject.b
's separated_chars
field is populated through a compute function called _compute_separated_chars
which is triggered by the change of sync.test.subject.b
's chars
field.
The role of sync.test.subject.c
is basically to set chars
by its own name
so that _compute_separated_chars
is triggered.
The "bug" is triggered by doing this:
- Create a new
sync.test.subject.c
input whatever name for exampleABCDEFG
- Save the new
sync.test.subject.c
- Call
action_set_char
by pressing theSet Name To Chars
button
You will see that the function is triggered twice.
Here is the result of the execution:
CHAR SEPARATION BEGIN
SEPARATE CHARS
DELETE CURRENT CHARS
DELETE EXISTING sync_test_subject_a THAT ARE RELATED TO CURRENT SELF
INSERT NEW CHAR RECORDS
UPDATE self.separated_chars WITH CHAR IDS
CHAR SEPARATION BEGIN
SEPARATE CHARS
DELETE CURRENT CHARS
DELETE EXISTING sync_test_subject_a THAT ARE RELATED TO CURRENT SELF
INSERT NEW CHAR RECORDS
UPDATE self.separated_chars WITH CHAR IDS
CHAR SEPARATION END
CHAR SEPARATION END
As the result the records which are only supposed to be A,B,C,D,E,F,G
doubled into A,B,C,D,E,F,G,A,B,C,D,E,F,G
. This is a really dangerous behavior because this can cause data crashes.
This is a detailed step by step layout on how this happens (based on the printout):
M1 = first call to the method
M2 = second call to the method
For example in this case
chars = ABCDEFG > trigger the method
M1 - CHAR SEPARATION BEGIN
M1 - SEPARATE CHARS
M1 tries to separate the chars into list [A,B,C,D,E,F,G]
M1 - DELETE CURRENT CHARS
This is needed to REPLACE current character records with the new ones.
since the `separated_chars` is currently empty nothing will be deleted
M1 - DELETE EXISTING sync_test_subject_a THAT ARE RELATED TO CURRENT SELF
The method will try to get all of the `sync.test.subject.a` ids that is related
to self by getting them from `separated_chars` so that they can be unlinked
(This happens before M1 - DELETE CURRENT CHARS).
Since nothing has been added to the `separated_chars`, this will not do anything
M1 - INSERT NEW CHAR RECORDS
Adding [A,B,C,D,E,F,G] `to sync.test.subject.a`, so `sync.test.subject.a` will have
[A,B,C,D,E,F,G]
M1 - UPDATE self.separated_chars WITH CHAR IDS
CURRENTLY THIS IS NOT YET EXECUTED, so no `sync.test.subject.a` ids has been assigned
to self. it will wait till M2 finish executing.
M2 - CHAR SEPARATION BEGIN
M2 - SEPARATE CHARS
M1 tries to separate the chars into list [A,B,C,D,E,F,G]
M2 - DELETE CURRENT CHARS
Because the `separated_chars` IS STILL EMPTY nothing happens.
M2 - DELETE EXISTING sync_test_subject_a THAT ARE RELATED TO CURRENT SELF
See, currently the `separated_chars` field IS STILL EMPTY THOUGH the
`sync.test.subject.a` is filled with [A,B,C,D,E,F,G] the method can't
get the ids because the `separated_chars` is empty.
M2 - INSERT NEW CHAR RECORDS
Now the method adds [A,B,C,D,E,F,G] `to sync.test.subject.a`, so
`sync.test.subject.a` will have [A,B,C,D,E,F,G,A,B,C,D,E,F,G]
M2 - UPDATE self.separated_chars WITH CHAR IDS
This will link `sync.test.subject.a` ids to self so now self has
[A,B,C,D,E,F,G]
M2 - CHAR SEPARATION END
Now after this M1 will continue linking the `sync.test.subject.a` ids to self
that means self will now have [A,B,C,D,E,F,G,A,B,C,D,E,F,G]
M1 - CHAR SEPARATION END
See the problem isn't how many times the method is executed but it's how the method calls itself when it tries to update the field. Which is nonsense.
Questions:
- Why is the
_compute_separated_chars
called twice at the same moment? - The trigger for
_compute_separated_chars
is supposed to be an update tochars
and thechars
is only updated ONCE, why is the method called twice?
Source Files: Source