3

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's separated_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 example ABCDEFG
  • Save the new sync.test.subject.c
  • Call action_set_char by pressing the Set 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 to chars and the chars is only updated ONCE, why is the method called twice?

Source Files: Source

William Wino
  • 3,599
  • 7
  • 38
  • 61

3 Answers3

2

I think that your problem is elsewhere. Never mind how many time Odoo invokes your _compute_separated_chars method you have to return correctly the value of the separated_chars field. And where is the problem for me?

The value of the separated_chars field is calculated inside your _compute_separated_chars method. So, in the time of invocation of this method the value of this field is undefined! You cannot rely on it. But you do - you use this value for deleting the existing records.

If you debug furthermore you'll see that when the method is executed, the value of separated_chars is probably empty. In this way you delete nothing. If you count the number of rows in your database table sync_test_subject_a you'll probably see that they are increasing with every execution of your _compute... method.

Try to elaborate a little bit different logic for your many2many relation field computation.

I'm giving you hereafter a cleaner version of your method but your problem still exists. The separated_chars are not unlinked because in the time of invocation self.separated_chars in empty!

@api.one
@api.depends('chars')
def _compute_separated_chars(self):
    a_model = self.env['sync.test.subject.a']
    if not self.chars:
        return
    self.separated_chars.unlink()
    for character in self.chars:
        self.separated_chars += \
                a_model.create({'name': character})
Andrei Boyanov
  • 2,309
  • 15
  • 18
  • Andrei thank you for taking the time to answer my question. The problem here isn't how MANY times the method is executed, if the method is executed sequentially no problem will arise. The problem is when the method tries call itself when I try to update the field, which is crazy because it will overlap its own method. The purpose of the method is to update the field, but when I try to update the field it calls itself again, BUT FOR WHAT? I've updated my answer so you can see how it works step by step. – William Wino Sep 10 '15 at 16:12
  • If I can somehow replace all of the many2many records AND also deleting all of the records that are related to those records AT THE SAME TIME, I probably wont need to clean up before I insert those new records. – William Wino Sep 11 '15 at 01:56
  • @William, let me say it again. The method `_compute_separated_chars` is not invoked by himself - it's invoked by the Odoo's mechanism of creating new object. If you are really curious why this happens I can investigate it for you. But your problem is elsewhere - you try to use something that's not yet defined! You cannot use `self.separated_chars` because it's not defined. It will be defined after your method returns. And by 'not defined' I mean it has no value! Did you check the number of rows in your database table `sync_test_subject_a` as I suggested you? – Andrei Boyanov Sep 11 '15 at 07:24
  • I've never said that is invoked by itself at first, it is invoked by itself at the end of the method execution and just before I update the field. Not only I've checked the number of rows, I've even written the resulted rows there in my description. I know that the table keeps growing, and I've explained why it keeps increasing there in my description. I know the self.separated_chars has yet defined and I've explained why I did what I did there in my description. If it doesn't call itself (doesn't overlap) how do you explain the double print "CHAR SEPARATION END" at the end of the execution? – William Wino Sep 11 '15 at 09:07
  • You need to understand the reason why I call `self.separated_chars` in the method. – William Wino Sep 11 '15 at 09:16
  • Cher @William, the reason your method is executed twice is that you are assigning to self.separated_chars ids, not objects of type sync.test.subject.a. There are other part of your code that you can also optimize. That's why I proposed you much shorter and cleaner code in my answer. Try to put this code in your loop `for character in characters: self.separated_chars += sync_test_subject_a_pool.create({'name':character})` and to delete the rest of you method. When you are happy with that we could discuss your real problem - how to delete many2many records in 'computed' field. – Andrei Boyanov Sep 13 '15 at 17:17
  • The method I proposed you is executed just once when editing a record. Please note that even my method is executed more than once when creating new method. – Andrei Boyanov Sep 13 '15 at 17:21
  • How does odoo justify executing the method twice for assigning ids to `separated_chars`? Assigning ids to separated chars doesn't change `chars` value and the condition for executing the method is supposed be a change to `chars` value, not assigning ids to `separated_chars` right? – William Wino Sep 14 '15 at 02:29
  • Wait could it be that odoo unlinks `self.separated_chars` BEFORE it calls `_compute_separated_chars`? – William Wino Sep 14 '15 at 03:20
  • Firstly, updating ANY field will trigger the compute method (looks like a lazy implementation because they don't know how to handle chain computed field changes so they just update every computed field every time there are changes to the other fields). Secondly, I can't update ANY field OTHER THAN the field that is related to compute method in compute method. I can't do anything to delete the leftover sync_test_subject_a records here. I'm at lost :( So yeah, back to the real problem, `How do I delete the leftover sync_test_subject_a records?` – William Wino Sep 14 '15 at 04:35
  • Here is the link to the newly created question related to my problem http://stackoverflow.com/questions/32557519/how-do-i-update-other-fields-or-another-models-from-inside-compute-function – William Wino Sep 14 '15 at 04:56
  • Andrei, have you seen my new question thread? I've put a bounty on it if you're interested in answering that question. – William Wino Sep 17 '15 at 01:25
  • Yes @William, I'll try to answer it today. – Andrei Boyanov Sep 17 '15 at 05:56
1

Allright, to sum this up. This problem has reached its dead end. I can't do anything else to fix this without editing Odoo's source code.

The unjustifiable things that Odoo does are as follows:

  1. The system strangely calls the compute method again from within the method itself if you assign ids to the compute field (which works by the way). (See Andrei's answer for an alternative way to assign many2many values that won't trigger this problem)
  2. The system restricts any changes to any other field or model except the field that related to the compute method from the compute method.
  3. The system updates the computed field everytime there are changes to any other field (even those fields that aren't related to the computed fields).

Andrei Boyanov's alternative implementation of the method fixes some of my problems. But there is one problem remains and I move the problem to Another Question Thread.

Community
  • 1
  • 1
William Wino
  • 3,599
  • 7
  • 38
  • 61
0

This behavior is actually caused by the method calling itself again every time I try to update the separated_chars field. What causes this behavior is unknown as the trigger of the method is the change of chars field as specified in this line of code: @api.depends('chars').

I dirty patched this with a lock field (Boolean) that is set True before updating the separated_chars field so that when _compute_separated_chars is called again it won't do anything.

Code:

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')
    lock            = fields.Boolean('Lock')

    @api.depends('chars')
    def _compute_separated_chars(self):
        if not self.lock:            

            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)

            self.lock = True

            print "UPDATE self.separated_chars WITH CHAR IDS"
            #UPDATE self.separated_chars WITH CHAR IDS
            self.separated_chars = separated_char_ids

            self.lock = False
            print "CHAR SEPARATION END"

sync_test_subject_b()

If anybody knows a better solution, feel free to post it in this thread.

William Wino
  • 3,599
  • 7
  • 38
  • 61