Simultaneous API updates some fields of a record (PATCH method) made unexpected result. (re-issue: #12338) #8022

Closed
opened 2025-12-29 20:31:21 +01:00 by adam · 2 comments
Owner

Originally created by @Tomoyuki-GH on GitHub (May 9, 2023).

NetBox version

v3.5.1 (netboxcommunity/netbox v3.5-2.6.0 docker image)

Python version

3.10

Steps to Reproduce

  1. prepare 2(or more) netbox API clients.

    • client X for updates a data field on a record(say record-A) with varying values continuously.

    • client Y for updates a data field (differ from client X) on the same record-A, with varying values continuously.

  2. run these clients(X, Y) simultaneously.

  3. see the result of record-A would be unexpected.

Expected Behavior

  • for example, counting up the field P from 1 to 50 with client X, and the field Q from 1 to 50 with client Y.
  • field P and Q are on the same data record(e.g. one on dicm.devices)
  • run the clients simultaneously and the time goes by, we expected the field P should be 50 and the field Q should be 50.

Observed Behavior

  • sometimes P: 50, Q: 46.
  • example code below (netbox_conflict_check.py) and the result shows conflicts.
    • expected vc_priority=38(actual 28), description=39(39), comments=37(35)
import argparse
import sys
import time
import json
import requests

config = {
    'NETBOX_URL': 'http://netbox:8080',
    'NETBOX_TOKEN': '0123456789abcdef0123456789abcdef01234567',
}

class NB(object):
    def __init__(self, url=None, token=None, headers={}, session=None):
        if session is None:
            self.session = requests.Session()
        self.url = url
        self.token = token
        self.headers = headers
        if token:
            self.session.headers.update({'Authorization': 'Token ' + str(token)})
        if headers:
            self.session.headers.update(headers)

    def get(self, endpoint, params={}):
        u = self.url + str(endpoint)
        s = self.session
        resp = s.get(u, params=params)
        if resp.content:
            return json.loads(resp.content)
        return {}

    def patch(self, endpoint, data={}):
        u = self.url + str(endpoint)
        s = self.session
        s.headers.update({'Content-Type': 'application/json'})
        resp = s.patch(u, data=json.dumps([data]))
        s.headers.pop('Content-Type')

        if resp.content:
            return json.loads(resp.content)
        return {}

def update_in_loop(nb, process_start, field):
    count = 0
    while time.time() - process_start < 10:
        start = time.time() - process_start
        count += 1
        server = nb.patch('/api/dcim/devices/', {'id': 1, field: count})

        end = time.time() - process_start
        # print(json.dumps(server, indent=2))

        print(f'PATCH {field}={count} | Start: {start:.5f}s | End: {end:.5f} | Virtual Chassis Priority: {server[0]["vc_priority"]} | Description: {server[0]["description"]} | Comments: {server[0]["comments"]}')

def get_in_loop(nb, process_start):
    count = 0
    while time.time() - process_start < 12:
        start = time.time() - process_start
        server = nb.get('/api/dcim/devices/1/')
        end = time.time() - process_start
        # print(json.dumps(server, indent=2))

        count += 1
        print(f'GET {count} | Start: {start:.5f}s | End: {end:.5f} | Virtual Chassis Priority: {server["vc_priority"]} | Description: {server["description"]} | Comments: {server["comments"]}')

def main():
    nb = NB(url=config['NETBOX_URL'],
            token=config['NETBOX_TOKEN'],
            headers={'Accept': 'application/json'})
    parser = argparse.ArgumentParser()
    parser.add_argument('update', nargs='?')
    args = parser.parse_args()
    process_start = time.time()
    if args.update:
        update_in_loop(nb, process_start, args.update)
    else:
        get_in_loop(nb, process_start)
    return 0

if __name__ == '__main__':
    sys.exit(main())

netbox_conflict_check-result.txt

Originally created by @Tomoyuki-GH on GitHub (May 9, 2023). ### NetBox version v3.5.1 (netboxcommunity/netbox v3.5-2.6.0 docker image) ### Python version 3.10 ### Steps to Reproduce 1. prepare 2(or more) netbox API clients. - client X for updates a data field on a record(say record-A) with varying values continuously. - client Y for updates a data field (differ from client X) on the same record-A, with varying values continuously. 1. run these clients(X, Y) simultaneously. 1. see the result of record-A would be unexpected. ### Expected Behavior - for example, counting up the field P from 1 to 50 with client X, and the field Q from 1 to 50 with client Y. - field P and Q are on the same data record(e.g. one on dicm.devices) - run the clients simultaneously and the time goes by, we expected the field P should be 50 and the field Q should be 50. ### Observed Behavior - sometimes P: 50, Q: 46. - example code below (netbox_conflict_check.py) and the result shows conflicts. - expected vc_priority=38(actual 28), description=39(39), comments=37(35) ```python import argparse import sys import time import json import requests config = { 'NETBOX_URL': 'http://netbox:8080', 'NETBOX_TOKEN': '0123456789abcdef0123456789abcdef01234567', } class NB(object): def __init__(self, url=None, token=None, headers={}, session=None): if session is None: self.session = requests.Session() self.url = url self.token = token self.headers = headers if token: self.session.headers.update({'Authorization': 'Token ' + str(token)}) if headers: self.session.headers.update(headers) def get(self, endpoint, params={}): u = self.url + str(endpoint) s = self.session resp = s.get(u, params=params) if resp.content: return json.loads(resp.content) return {} def patch(self, endpoint, data={}): u = self.url + str(endpoint) s = self.session s.headers.update({'Content-Type': 'application/json'}) resp = s.patch(u, data=json.dumps([data])) s.headers.pop('Content-Type') if resp.content: return json.loads(resp.content) return {} def update_in_loop(nb, process_start, field): count = 0 while time.time() - process_start < 10: start = time.time() - process_start count += 1 server = nb.patch('/api/dcim/devices/', {'id': 1, field: count}) end = time.time() - process_start # print(json.dumps(server, indent=2)) print(f'PATCH {field}={count} | Start: {start:.5f}s | End: {end:.5f} | Virtual Chassis Priority: {server[0]["vc_priority"]} | Description: {server[0]["description"]} | Comments: {server[0]["comments"]}') def get_in_loop(nb, process_start): count = 0 while time.time() - process_start < 12: start = time.time() - process_start server = nb.get('/api/dcim/devices/1/') end = time.time() - process_start # print(json.dumps(server, indent=2)) count += 1 print(f'GET {count} | Start: {start:.5f}s | End: {end:.5f} | Virtual Chassis Priority: {server["vc_priority"]} | Description: {server["description"]} | Comments: {server["comments"]}') def main(): nb = NB(url=config['NETBOX_URL'], token=config['NETBOX_TOKEN'], headers={'Accept': 'application/json'}) parser = argparse.ArgumentParser() parser.add_argument('update', nargs='?') args = parser.parse_args() process_start = time.time() if args.update: update_in_loop(nb, process_start, args.update) else: get_in_loop(nb, process_start) return 0 if __name__ == '__main__': sys.exit(main()) ``` [netbox_conflict_check-result.txt](https://github.com/netbox-community/netbox/files/11430421/netbox_conflict_check-result.txt)
adam added the status: duplicate label 2025-12-29 20:31:21 +01:00
adam closed this issue 2025-12-29 20:31:21 +01:00
Author
Owner

@jeremystretch commented on GitHub (May 9, 2023):

As I commented on #12338, this is not an actionable bug report. Please do not open duplicate issues. If you would like to propose a specific change that you believe is necessary, please describe in detail what it is, and we'll be happy to discuss. Otherwise, this issue will remain closed.

@jeremystretch commented on GitHub (May 9, 2023): As I commented on #12338, this is not an actionable bug report. Please do not open duplicate issues. If you would like to propose a specific change that you believe is necessary, please describe in detail what it is, and we'll be happy to discuss. Otherwise, this issue will remain closed.
Author
Owner

@kkthxbye-code commented on GitHub (May 9, 2023):

I don't think anyone was confused about what happened in the other issue. As Jeremy said, you are free to provide a proposed improvement.

If you'd like to propose a specific improvement that may help mitigate these concerns (without significantly impacting performance), we'd be happy to discus.

See:

https://github.com/encode/django-rest-framework/issues/4675

https://github.com/encode/django-rest-framework/pull/8489

http://chibisov.github.io/drf-extensions/docs/#partialupdateserializermixin

As for an immediate workaround for you, you can run your application server with one process/thread.

@kkthxbye-code commented on GitHub (May 9, 2023): I don't think anyone was confused about what happened in the other issue. As Jeremy said, you are free to provide a proposed improvement. > If you'd like to propose a specific improvement that may help mitigate these concerns (without significantly impacting performance), we'd be happy to discus. See: https://github.com/encode/django-rest-framework/issues/4675 https://github.com/encode/django-rest-framework/pull/8489 http://chibisov.github.io/drf-extensions/docs/#partialupdateserializermixin As for an immediate workaround for you, you can run your application server with one process/thread.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/netbox#8022