-
-
Notifications
You must be signed in to change notification settings - Fork 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
[JENKINS-45892] Prevent model objects from being serialized except at top level #2957
Conversation
return this; | ||
} else { | ||
// Unfortunately we cannot easily tell which XML file is actually being saved here, at least without implementing a custom Converter. | ||
LOGGER.log(WARNING, "JENKINS-45892: reference to {0} being saved but not at top level", this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that in the common case that you just have a [Run]Action
storing a (nontransient
) Run owner
, writeReplace
will get called only once—at top level—and we proceed normally. XStream then writes a reference="../../.."
or similar, and deserialization works. We would rather print a warning in this case, but I could find no way to detect this case: AbstractReferenceMarshaller.convert
runs and does not offer us any chance to intercept the process. This patch therefore only changes behavior in other cases:
- A build stored via some reference chain in another build, as in the test.
- A build stored in some unrelated object.
- A build stored in another copy of itself.
The third is the problematic case for lazy-loading. I am not sure of the exact sequence of events (which is why I have no test for it), but somehow a build gets loaded, a badly written action is attached to it, the build is GC’d, a new Run
object is reloaded from disk, but we have the original Action
somewhere, and Run.save
is called on one copy while another copy is referred to internally. Then instead of reference
, XStream writes out a new nested <build>
element. When deserialized, this explodes because onLoad
is not called on the nested Run
and so project
is left null…as seen in in the toString
patch.
This pull request originates from a CloudBees employee. At CloudBees, we require that all pull requests be reviewed by other CloudBees employees before we seek to have the change accepted. If you want to learn more about our process please see this explanation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor 🐛 for the replacer logic, but the approach generally looks good to me.
I would suggest moving the logic to the Restricted API classes, because there are use-case in other Jenkins model objects.
I also think a changelog entry is mandatory since the change implies a significant regression risk
synchronized (saving) { | ||
if (saving.contains(this)) { | ||
return this; | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do you need to keep the else branch synchronized?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could limit the monitor scope to the contains
call, but should not matter—neither the logging nor the Replacer.<init>
should block.
} | ||
private Object readResolve() { | ||
// May or may not work: | ||
return Jenkins.getInstance().getItemByFullName(fullName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From what I see this call may happen when Jenkins instance is null and when the jobs are not fully loaded. It would be better to handle this cases explicitly 🐛
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It may indeed happen when jobs are not fully loaded, in which case the replacement is null, as seen in the test. It should not happen when Jenkins.instance == null
because that would be before startup even, well, starts.
saving.add(this); | ||
} | ||
try { | ||
getConfigFile().write(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
code dup. would be better to move it to private saveInternal()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private
? I guess you mean, public
but @Restricted(NoExternalUse.class)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fine as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be final then though
saving.add(this); | ||
} | ||
try { | ||
getConfigFile().write(this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to move the save/replacer logic into a shared Impl class + Interface with default methods? It seems to be a common use-case for Jenkins which could be reused in other model objects. Having it in external class may help, especially when/if somebody starts working on pluggable storage stories in the core
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default methods are not an option since the saving
list cannot be kept in an interface. Could be in a utility class somewhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or JustAnotherMixIn
contributed via interface
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🐝 resolver logic should work in such way. If not, we will get nice NPEs later at least
@reviewbybees done |
See JENKINS-45892.
Run
AbstractItem
User
Proposed changelog entries
@reviewbybees