-
-
Notifications
You must be signed in to change notification settings - Fork 6.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
PUT calls don't fully "replace the state of the target resource" #4231
Comments
Okay, can't promise I'll be able to review this pretty in-depth ticket immediately, as the upcoming 3.4 release takes priority. But thanks for such a detailed, well thought through issue. This'll most likely like be looked at on the scale of weeks, not days or months. If you make any progress, have any further thoughts yourself, please do update the ticket and keep us informed. |
OK. I'm pretty sure my issue is the combination of two factors:
As a result, DRF uses the current (database) value and breaks the concurrency control. The second half of the issue is related to the discussion in #3648 (also cited above) and there's a (pre 3.x) discussion in #1445 that still appears to be relevant. I'm hoping a concrete (and increasingly common) case where the default behavior is perverse will be enough to reopen the discussion about the "ideal" behavior of a ModelSerializer. Obviously, I'm only an inch deep on DRF, but my intuition is that the following behavior is appropriate for a required field and a PUT:
We can't change bullet (1) above or the defaults become useless (we require the input even though we know the default). That means we have to fix the issue by changing #2 above. I agree with your argument in #2683 that:
To be consistent with that separation of concerns, update should create a new instance (delegating all defaults to the model) and apply the submitted values to that new instance. This results in the behavior requested in #3648. Trying to describe the migration path helps highlight how odd the current behavior is. The end goal is to
What is the name of that flag? The current Model Serializer is actually a partial serializer that (somewhat arbitrarily) requires fields that meet the condition |
You can add
|
Thanks @anoopmalev. That'll keep me on the production branch. After "sleeping on it" I realize there's an extra wrinkle. Everything I said should apply to the serializer's fields. If a field isn't included in the serializer, it should not be modified. In this way, all serilaizers are (and should be) partial for the non-included fields. This is a little more complicated than my "make a new instance" above. |
I believe this issue needs to be reduced to a more constrained proposal in order to move forward. |
Here's a concise proposal... for a non-partial serializer:
For clarity, validation is run on the final product of this process. |
Ie you want to set Have I got that correct? |
Yes (and more). That's how I understand the The italicized section is what DRF is not currently doing and the more important change in my proposal. The current implementation just skips the field. I had a second proposal mixed in, but it's really a separate question of how generous you want to be with the idea of a "default". The current behavior is "strict" in that only a |
@claytondaley I'm using OOL with DRF since 2x this way: class VersionModelSerializer(serializers.ModelSerializer, BaseSerializer):
_initial_version = 0
_version = VersionField()
def __init__(self, *args, **kwargs):
super(VersionModelSerializer, self).__init__(*args, **kwargs)
# version field should not be required if there is no object
if self.instance is None and '_version' in self.fields and\
getattr(self, 'parent', None) is None:
self.fields['_version'].read_only = True
self.fields['_version'].required = False
# version field is required while updating instance
if self.instance is not None and '_version' in self.fields:
self.fields['_version'].required = True
if self.instance is not None and hasattr(self.instance, '_version'):
self._initial_version = self.instance._version
def validate__version(self, value):
if self.instance is not None:
if not value and not isinstance(value, int):
raise serializers.ValidationError(_(u"This field is required"))
return value
# more code & helpers it works just great with all kind of business logic and never caused any problem. |
Was this left closed on accident? I responded to the specific question and didn't hear a reason what was wrong with the proposal. |
@claytondaley why OOL should be a part of DRF? Check my code – it works just find in a large app (1400 tests). |
You've hard-coded the OOL into the serializer. This is the wrong place to do it because you have a race condition. Parallel updates (with the same prior version) would all pass at the serializer... but only one would win at the save action. I'm using
When we don't require the field, we do so because we have a default to use. DRF doesn't fulfill that contract because it doesn't use the default... it uses the existing value. The underlying issue was discussed before, but they didn't have a nice, concrete case. OOL is that ideal case. The existing value of a version field always passes OOL so you can bypass the entire OOL system by leaving out version. That's (obviously) not the desired behavior of an OOL system. |
Did I? Have you found any OOL logic in my serializer beside field requirement?
Sry, I just cant see where is the race condition here.
I'm also using
|
actually, im not using |
here is the missing def save(self, **kwargs):
try:
self.instance = super(VersionModelSerializer, self).save(**kwargs)
return self.instance
except VersionException:
# Use select_for_update so we have some level of guarantee
# that object won't be modified at least here at the same time
# (but it may be modified somewhere else, where select_for_update
# is not used!)
with transaction.atomic():
db_instance = self.instance.__class__.objects.\
select_for_update().get(pk=self.instance.pk)
diff = self._get_serializer_diff(db_instance)
# re-raise exception, so api client will receive friendly
# printed diff with writable fields of current serializer
if diff:
raise VersionException(diff)
# otherwise re-try saving using db_instance
self.instance = db_instance
if self.is_valid():
return super(VersionModelSerializer, self).save(**kwargs)
else:
# there are errors that could not be displayed to a user
# so api client should refresh & retry by itself
raise VersionException
# instance.save() was interrupted by application error
except ApplicationException as logic_exc:
if self._initial_version != self.instance._version:
raise VersionException
raise logic_exc |
Sorry. I didn't read your code to figure out what you were doing. I saw a serializer. You can obviously work around the issue by hacking the serializer but you shouldn't have to.... because the flaw in the DRF logic stands on its own. I'm just using OOL to make the point. |
And you should try that code against the latest version of django-concurrency (using |
I think it's called extending default functionality, not really hacking. I think the best place for such feature support is at I've reread whole issue discussion and found your proposal too broad and it would fail in many places (due to magically using default values from different sources under different conditions). DRF 3.x just got much easier and predictable than 2.x, lets keep it that way :) |
You can't fix this in the model layer because it's broken at the serializer (before it gets to the model). Set OOL aside... why don't we require a field if |
A non partial serializer "requires" all fields (fundamentally) and yet we let this one by. Is it a bug? Or do we have a logical reason? |
as you can see in my code example – _version field is always correctly required in all possible cases. btw, it turned out that I've borrowed model lvl code from https://github.com/gavinwahl/django-optimistic-lock and not from |
... so the bug is "non-partial serializers incorrectly set some fields to not-required". That's the alternative. Because that's the (implicit) commitment that a non-partial serializer makes. |
I can quote it:
This says nothing about required (except when a default is provided). |
(and I get that I'm talking about two different levels, but the ModelSerializer shouldn't un-require fields if it's not going to take responsibility for that decision) |
I think I've lost your point.. |
Whats wrong with that? |
OK let me try a different angle.
Should a CREATE or UPDATE with the same data ever produce a different object (minus the ID) |
I'm going to de-milestone this for now. We can reassess after v3.7 |
Up to you guys, but I want to make sure you're clear that this is not a Ticket to add concurrency support. The real issue is that a single serializer cannot correctly validate both a PUT and POST in the current architecture. Concurrency just provided the "failing test". |
TL;DR You can see why this issue is blocked by starting at Tom's proposed fix. In summary, the proposed solution is to make all fields required for a
The real issue is how we handle missing data and the basic proposal in #3648, #4703, and here remain the right solution. We can support all of the HTTP modes (including create-by- |
There's nothing preventing anyone from implementing either this, or a strict "require all fields" as a base serializer class, and wrapping that up as a third party library. My opinion is that a strict "require all fields" mode might also be able to make it into core, it's very clear obvious behavior, and I can see why that'd be useful. I'm not convinced that a "allow fields to be optional, but replace everything, using model defaults if they exist" - That seems like it'd present some very counter-intuitive behavior (eg. "created_at" fields, that automatically end up updating themselves). If we want a stricter behavior, we should just have a stricter behavior. Either way around, the right way to approach this is to validate it as a third party package, then update our docs so we can link to that. Alternatively, if you're convinced that we're missing a behavior from core that our users really do need, then you're welcome to make a pull request, updating the behavior and the documentation, so we can assess the merits in a very concrete way. Happy to take pull requests as a starting point for this, and even happier to include a third party package demonstrating this behavior. |
This still stands. I think we could consider that aspect in core, if someone cares enough about it to make a pull request along those lines. It'd need to be an optional behavior. |
A
Absolutely. The "use defaults" variation is an ideal case for a 3rd party package because the change is a trivial wrapper around (one method of) the existing behavior and (if you buy into the defaults argument) works for all non-partial serializers.
Perhaps you'd consider adding a label like "PR Welcome" or "3rd Party Plugin" and leaving valid/acknowledged issues like this open. I often search open issues to see if a problem has already been reported and its progress towards resolution. I perceive closed issues as "invalid" or "fixed". Mixing a few "valid but closed" issues into the thousands of invalid/fixed issues doesn't invite efficient searching (even if you knew they might be there). |
That'd be reasonable enough, but we'd like our issue tracker to reflect active or actionable work on the project itself. It's really important for us to try to keep our issues tightly scoped. Changing priorities might mean that we sometime choose to reopen issues that we've previously closed. Right now I think this has fallen out of the "the core team want to address this in the immediate future". If it comes up repeatedly, and there continues to be no third party solution, then perhaps we would reassess it. |
A bit more context on the issue management style - https://www.dabapps.com/blog/sustainable-open-source-management/ |
EDIT: For the current status of the issue, skip to #4231 (comment)
===
I'm having an issue implementing an Optimistic Concurrency library on an application that uses DRF to interact with the database. I'm trying to:
I recently added optimistic concurrency to my Django application. To save you the Wiki lookup:
I had a legacy UI talking through DRF. The legacy UI did not handle version numbers. I expected this to cause concurrency errors, but it did not. If I understand the discussion in #3648 correctly:
There are no easy options (like making the field "required") to ensure the data is submitted every time.(edit: you can workaround the issue by making it required as demonstrated in this comment)Steps to reproduce
Expected behavior
The missing version ID should not match the database and cause a concurrency issue.
Actual behavior
The missing version ID is filled by DRF with the current ID so the concurrency check passes.
The text was updated successfully, but these errors were encountered: