Skip to content
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-56937 JCasC support for admin monitors #4552

Merged
merged 13 commits into from
Apr 1, 2020

Conversation

timja
Copy link
Member

@timja timja commented Mar 7, 2020

See JENKINS-56937.

Example config:

jenkins:
  disabledAdministrativeMonitors:
    - "jenkins.security.s2m.MasterKillSwitchWarning"

Proposed changelog entries

  • Entry 1: JENKINS-56937, configuration-as-code plugin support for disable admin monitors
  • ...

Proposed upgrade guidelines

N/A

Submitter checklist

  • JIRA issue is well described
  • Changelog entries and upgrade guidelines are appropriate for the audience affected by the change (users or developer, depending on the change). Examples
    • Fill-in the Proposed changelog entries section only if there are breaking changes or other changes which may require extra steps from users during the upgrade
  • Appropriate autotests or explanation to why this change has no tests
  • For dependency updates: links to external changelogs and, if possible, full diffs

Desired reviewers

@mention

Maintainer checklist

Before the changes are marked as ready-for-merge:

  • There are at least 2 approvals for the pull request and no outstanding requests for change
  • Conversations in the pull request are over OR it is explicit that a reviewer does not block the change
  • Changelog entries in the PR title and/or Proposed changelog entries are correct
  • Proper changelog labels are set so that the changelog can be generated automatically
  • If the change needs additional upgrade steps from users, upgrade-guide-needed label is set and there is a Proposed upgrade guidelines section in the PR title. (example)
  • If it would make sense to backport the change to LTS, a JIRA issue should exist and be labeled as lts-candidate

*
* @since TODO
*/
public Set<String> getDisabledAdministrativeMonitors(){
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what's buggy about the CopyOnWriteArraySet?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what's buggy about the CopyOnWriteArraySet?

JCasC doesn't support it

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to avoid sharing the same data structure w/ the UI, why not just do something like this?

  synchronized(this.disabledAdministrativeMonitors) { return new HashSet<String>(this.disabledAdministrativeMonitors); };

... then any client of this class doesn't need to worry about concurrency concerns in the internal data structure; those concerns remain .. internal to this implementation

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that clients modify this set, so returning a copy prevents them updating it

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ouch. any reason that clients can't be modified to invoke the setter instead of relying on directly mutating the internal state of this object? the current situation sounds like a pretty leaky design

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggested modifying it a couple of comments up, will take a look

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have changed, PTAL

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggest wrapping this return ... statement w/ synchronized(disabledAdministrativeMonitors) {} as suggested above, since the new HashSet<>(..) operation will iterate over the collection passed to it

Copy link
Member

@jetersen jetersen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@timja timja requested review from a team and oleg-nenashev March 7, 2020 11:32
* @since TODO
*/
public void setDisabledAdministrativeMonitors(Set<String> disabledAdministrativeMonitors){
this.disabledAdministrativeMonitors = disabledAdministrativeMonitors;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should be cloned here instead of taking a reference to an external object

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why’s that? It’s just a setter

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both stapler and jcasc will handle creating a clone that should be safe for Jenkins core to use.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Spotbugs normally warns against taking a reference to an outside object

Copy link
Member

@jetersen jetersen Mar 8, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not on collections or simple object types.

Spotbugs would complain on date objects.

@@ -93,13 +93,26 @@ protected void killComputer(Computer c) {
* Package-protected, but accessed API
* ============================================================================================================== */

/*package*/ final CopyOnWriteArraySet<String> disabledAdministrativeMonitors = new CopyOnWriteArraySet<>();
private Set<String> disabledAdministrativeMonitors = new HashSet<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a gigantic comment above that you might want to consider removing

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was probably already obsolete after #3873

@daniel-beck daniel-beck self-requested a review March 7, 2020 22:29
Copy link
Member

@alecharp alecharp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the visibility change but I'm not so sure about the type change..

*
* @since TODO
*/
public void setDisabledAdministrativeMonitors(Set<String> disabledAdministrativeMonitors){
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public void setDisabledAdministrativeMonitors(Set<String> disabledAdministrativeMonitors){
public void setDisabledAdministrativeMonitors(Set<String> disabledAdministrativeMonitors) {

Copy link
Member

@rsandell rsandell left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes me slightly nervous removing the thread secure CopyOnWriteArraySet to a non thread safe HashSet

@daniel-beck
Copy link
Member

@timja
Copy link
Member Author

timja commented Mar 12, 2020

Can we keep it a CopyOnWriteArraySet internally to not break when a badly timed nisarg1499/Jenkins:core/src/main/java/hudson/model/AdministrativeMonitor.java@b2f718f#L121-L122 arrives?

probably, i'll take a look

@timja timja added the rfe For changelog: Minor enhancement. use `major-rfe` for changes to be highlighted label Mar 14, 2020
@timja
Copy link
Member Author

timja commented Mar 14, 2020

Can we keep it a CopyOnWriteArraySet internally to not break when a badly timed nisarg1499/Jenkins:core/src/main/java/hudson/model/AdministrativeMonitor.java@b2f718f#L121-L122 arrives?

Is what I've pushed what you meant?

@daniel-beck
Copy link
Member

Is what I've pushed what you meant?

Sort of. I'd also sprinkle in some volatile and/or synchronized until it looks about right 😁

Copy link
Member Author

@timja timja left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how's this @daniel-beck ?

* ============================================================================================================== */

/*package*/ final CopyOnWriteArraySet<String> disabledAdministrativeMonitors = new CopyOnWriteArraySet<>();
private Set<String> disabledAdministrativeMonitors = new CopyOnWriteArraySet<>();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private Set<String> disabledAdministrativeMonitors = new CopyOnWriteArraySet<>();
private volatile Set<String> disabledAdministrativeMonitors = new CopyOnWriteArraySet<>();

@daniel-beck
Copy link
Member

Better to ask someone else.

@timja timja requested review from jvz and jglick March 15, 2020 16:04
@timja
Copy link
Member Author

timja commented Mar 15, 2020

Better to ask someone else.

@jglick or @jvz any chance you could take a look at the concurrency concern?

@jvz
Copy link
Member

jvz commented Mar 16, 2020

If you want to make that correct, you'll need to make both the getter and setter synchronized as well as any other (public or externally-callable) methods that access or mutate the set. At that point, you can also just use HashSet since everything would be protected by the synchronization mutex.

Alternatively, you could work at being almost correct by changing the setter to do a clear() followed by an addAll() rather than changing the field itself. Then no synchronization or volatile is needed, and it only introduces a fairly insignificant race condition.

@timja
Copy link
Member Author

timja commented Mar 16, 2020

If you want to make that correct, you'll need to make both the getter and setter synchronized as well as any other (public or externally-callable) methods that access or mutate the set. At that point, you can also just use HashSet since everything would be protected by the synchronization mutex.

Alternatively, you could work at being almost correct by changing the setter to do a clear() followed by an addAll() rather than changing the field itself. Then no synchronization or volatile is needed, and it only introduces a fairly insignificant race condition.

Thanks, minding taking a look at what I pushed to see if that would work

* ============================================================================================================== */

/*package*/ final CopyOnWriteArraySet<String> disabledAdministrativeMonitors = new CopyOnWriteArraySet<>();
private volatile Set<String> disabledAdministrativeMonitors = new HashSet<>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My mistake; this will likely still need to be either CopyOnWriteArraySet or use Collections.synchronizedSet() around this. Otherwise, a caller that iterates on the result of getDisabledAdministrativeMonitors() may get a ConcurrentModificationException when another thread calls disable() which modifies the set.

Alternatively, you can update getDisabledAdministrativeMonitors to wrap the set in a new HashSet to copy the set and prevent the issue entirely.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, you can update getDisabledAdministrativeMonitors to wrap the set in a new HashSet to copy the set and prevent the issue entirely.

That doesn't work as the set is modified through the getter, by calling add on it.

Copy link
Member

@jvz jvz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, just saw your comment. If you're also modifying the returned result, then this still won't work properly. What you can do instead would be to store a Collections.synchronizedSet(new HashSet<>()) in the field (make it final). Next, the methods that were previously synchronized should instead synchronized (disabledAdministrativeMonitors) in a block inside the method (remove synchronized from the method keywords). Then callers adding to the set will be synchronized on the same lock. Finally, in the setter, you should (inside the synchronized (disabledAdministrativeMonitors) { ... } block) clear followed by addAll.

@timja timja requested a review from jvz March 16, 2020 18:43
@jvz
Copy link
Member

jvz commented Mar 16, 2020

Thanks for your patience with my confusion here. I believe this last change I suggested should put things in the proper place!

@timja timja added the squash-merge-me Unclean or useless commit history, should be merged only with squash-merge label Mar 16, 2020
@timja timja requested a review from jvz March 16, 2020 18:58
@timja timja requested review from rsandell, alecharp and res0nance and removed request for jglick March 16, 2020 20:12
/* =================================================================================================================
* Package-protected, but accessed API
* ============================================================================================================== */
private final Set<String> disabledAdministrativeMonitors = Collections.synchronizedSet(new HashSet<>());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs of synchronizedSet says (I've never used this before)

Returns a synchronized (thread-safe) set backed by the specified set. In order to guarantee serial access, it is critical that all access to the backing set is accomplished through the returned set.

It is imperative that the user manually synchronize on the returned set when iterating over it:

  Set s = Collections.synchronizedSet(new HashSet());
      ...
  synchronized (s) {
      Iterator i = s.iterator(); // Must be in the synchronized block
      while (i.hasNext())
          foo(i.next());
  }
 

Failure to follow this advice may result in non-deterministic behavior. 

I'm not sure what it means by Failure to follow this advice may result in non-deterministic behavior. in here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no direct iteration of this field,

there's 4 usages of it,

  • enable / disable, calls add /remove
  • isEnabled, calls contains
  • the UI, which iterates over it, not sure if there's much we can do there,
  • jcasc, it will use the setter to update it which will clear the set and use what jcasc sets.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My worry is what is the worst that could happen? Will it throw an exception or will it just show old data?
If its the latter then I think its alright but the documentation doesn't seem perfectly clear

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#3687 (comment) was the (un)funnest one I've seen. Way up there in my hall of shame ☹️

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's only relevant because iterating over a collection is technically n operations, not 1. You need to synchronize for the duration of the logical operation, not n separate synchronizations for n items in the collection. This is all about fencing the allowed execution timeline based on "happens-before" rules in Java.

So, in a sense, no, peppering synchronized and volatile everywhere isn't good enough to solve every concurrency issue! 😅

@timja timja requested a review from res0nance March 17, 2020 08:08
@jdef
Copy link

jdef commented Mar 24, 2020 via email

@timja timja requested a review from jvz March 25, 2020 08:50
Copy link
Member

@jvz jvz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it would work well enough, too. Thanks for synchronizing on a final field, too, which is the recommended approach in general.

@timja timja added the ready-for-merge The PR is ready to go, and it will be merged soon if there is no negative feedback label Mar 30, 2020
@timja
Copy link
Member Author

timja commented Mar 30, 2020

Thanks for the reviews all, marking as ready-for-merge, this will be merged after 24 hours if there's no negative feedback

@timja timja merged commit 1a0f231 into jenkinsci:master Apr 1, 2020
@timja timja deleted the disable-admin-monitors branch April 1, 2020 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ready-for-merge The PR is ready to go, and it will be merged soon if there is no negative feedback rfe For changelog: Minor enhancement. use `major-rfe` for changes to be highlighted squash-merge-me Unclean or useless commit history, should be merged only with squash-merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants