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

[FIXED JENKINS-40252] Add an Iterable<T> that returns all items unsorted #2665

Merged
merged 5 commits into from Dec 16, 2016
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/DependencyRunner.java
Expand Up @@ -59,7 +59,7 @@ public void run() {
Set<AbstractProject> topLevelProjects = new HashSet<AbstractProject>();
// Get all top-level projects
LOGGER.fine("assembling top level projects");
for (AbstractProject p : Jenkins.getInstance().getAllItems(AbstractProject.class))
for (AbstractProject p : Jenkins.getInstance().allItems(AbstractProject.class))
if (p.getUpstreamProjects().size() == 0) {
LOGGER.fine("adding top level project " + p.getName());
topLevelProjects.add(p);
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/java/hudson/model/Computer.java
Expand Up @@ -778,7 +778,7 @@ public List<AbstractProject> getTiedJobs() {
}

public RunList getBuilds() {
return new RunList(Jenkins.getInstance().getAllItems(Job.class)).node(getNode());
return RunList.fromJobs(Jenkins.getInstance().allItems(Job.class)).node(getNode());
}

/**
Expand Down
6 changes: 1 addition & 5 deletions core/src/main/java/hudson/model/DependencyGraph.java
Expand Up @@ -91,7 +91,7 @@ public void build() {
SecurityContext saveCtx = ACL.impersonate(ACL.SYSTEM);
try {
this.computationalData = new HashMap<Class<?>, Object>();
for( AbstractProject p : getAllProjects() )
for( AbstractProject p : Jenkins.getInstance().allItems(AbstractProject.class) )
p.buildDependencyGraph(this);

forward = finalize(forward);
Expand Down Expand Up @@ -147,10 +147,6 @@ public int compare(AbstractProject<?,?> o1, AbstractProject<?,?> o2) {
topologicallySorted = Collections.unmodifiableList(topologicallySorted);
}

Collection<AbstractProject> getAllProjects() {
return Jenkins.getInstance().getAllItems(AbstractProject.class);
}

/**
* Special constructor for creating an empty graph
*/
Expand Down
227 changes: 205 additions & 22 deletions core/src/main/java/hudson/model/Items.java
Expand Up @@ -30,29 +30,30 @@
import hudson.model.listeners.ItemListener;
import hudson.remoting.Callable;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.security.AccessControlled;
import hudson.triggers.Trigger;
import hudson.util.DescriptorList;
import hudson.util.EditDistance;
import hudson.util.XStream2;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.apache.commons.lang.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.StringTokenizer;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

import jenkins.model.DirectlyModifiableTopLevelItemGroup;
import jenkins.model.Jenkins;
import org.acegisecurity.Authentication;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;

/**
* Convenience methods related to {@link Item}.
Expand All @@ -79,6 +80,44 @@ public class Items {
return false;
}
};
/**
* A comparator of {@link Item} instances that uses a case-insensitive comparison of {@link Item#getName()}.
Copy link
Member

Choose a reason for hiding this comment

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

Wouldn't a natural sort be better? IIRC a user recently requested this.

I.e.

item1
item2
item10

Copy link
Member Author

Choose a reason for hiding this comment

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

You can do whatever sorts you want, this is the existing sort that you need to restore the sort order from getAllItems() if you use allItems() to build a filtered sub-list

* If you are replacing {@link #getAllItems(ItemGroup, Class)} with {@link #allItems(ItemGroup, Class)} and
* need to restore the sort order of a further filtered result, you probably want {@link #BY_FULL_NAME}.
*
* @since FIXME
*/
public static final Comparator<Item> BY_NAME = new Comparator<Item>() {
@Override public int compare(Item i1, Item i2) {
return name(i1).compareToIgnoreCase(name(i2));
}

String name(Item i) {
String n = i.getName();
if (i instanceof ItemGroup) {
n += '/';
}
return n;
}
};
/**
* A comparator of {@link Item} instances that uses a case-insensitive comparison of {@link Item#getFullName()}.
*
* @since FIXME
*/
public static final Comparator<Item> BY_FULL_NAME = new Comparator<Item>() {
@Override public int compare(Item i1, Item i2) {
return name(i1).compareToIgnoreCase(name(i2));
}

String name(Item i) {
String n = i.getFullName();
if (i instanceof ItemGroup) {
n += '/';
}
return n;
}
};

/**
* Runs a block while making {@link #currentlyUpdatingByXml} be temporarily true.
Expand Down Expand Up @@ -350,7 +389,12 @@ public static XmlFile getConfigFile(Item item) {

/**
* Gets all the {@link Item}s recursively in the {@link ItemGroup} tree
* and filter them by the given type.
* and filter them by the given type. The returned list will represent a snapshot view of the items present at some
* time during the call. If items are moved during the call, depending on the move, it may be possible for some
* items to escape the snapshot entirely.
* <p>
* If you do not need to iterate all items, or if the order of the items is not required, consider using
* {@link #allItems(ItemGroup, Class)} instead.
*
* @since 1.512
*/
Expand All @@ -361,18 +405,8 @@ public static <T extends Item> List<T> getAllItems(final ItemGroup root, Class<T
}
private static <T extends Item> void getAllItems(final ItemGroup root, Class<T> type, List<T> r) {
List<Item> items = new ArrayList<Item>(((ItemGroup<?>) root).getItems());
Collections.sort(items, new Comparator<Item>() {
@Override public int compare(Item i1, Item i2) {
return name(i1).compareToIgnoreCase(name(i2));
}
String name(Item i) {
String n = i.getName();
if (i instanceof ItemGroup) {
n += '/';
}
return n;
}
});
// because we add items depth first, we can use the quicker BY_NAME comparison
Collections.sort(items, BY_NAME);
for (Item i : items) {
if (type.isInstance(i)) {
if (i.hasPermission(Item.READ)) {
Expand All @@ -385,6 +419,41 @@ String name(Item i) {
}
}

/**
* Gets a read-only view of all the {@link Item}s recursively in the {@link ItemGroup} tree visible to
* {@link Jenkins#getAuthentication()} without concern for the order in which items are returned. Each iteration
* of the view will be "live" reflecting the items available between the time the iteration was started and the
* time the iteration was completed, however if items are moved during an iteration - depending on the move - it
* may be possible for such items to escape the entire iteration.
*
Copy link
Member

Choose a reason for hiding this comment

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

Makes sense to mention the iterator is read-inly

* @param root the root.
* @param type the type.
* @param <T> the type.
* @return An {@link Iterable} for all items.
* @since FIXME
*/
public static <T extends Item> Iterable<T> allItems(ItemGroup root, Class<T> type) {
return allItems(Jenkins.getAuthentication(), root, type);
}


/**
* Gets a read-only view all the {@link Item}s recursively in the {@link ItemGroup} tree visible to the supplied
* authentication without concern for the order in which items are returned. Each iteration
* of the view will be "live" reflecting the items available between the time the iteration was started and the
* time the iteration was completed, however if items are moved during an iteration - depending on the move - it
* may be possible for such items to escape the entire iteration.
*
* @param root the root.
* @param type the type.
* @param <T> the type.
* @return An {@link Iterable} for all items.
* @since FIXME
Copy link
Member

Choose a reason for hiding this comment

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

Makes sense to mention the iterator is read-inly

Copy link
Member

Choose a reason for hiding this comment

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

fixed

*/
public static <T extends Item> Iterable<T> allItems(Authentication authentication, ItemGroup root, Class<T> type) {
return new AllItemsIterable<>(root, authentication, type);
}

/**
* Finds an item whose name (when referenced from the specified context) is closest to the given name.
* @param <T> the type of item being considered
Expand All @@ -395,10 +464,9 @@ String name(Item i) {
* @since 1.538
*/
public static @CheckForNull <T extends Item> T findNearest(Class<T> type, String name, ItemGroup context) {
List<T> projects = Jenkins.getInstance().getAllItems(type);
String[] names = new String[projects.size()];
for (int i = 0; i < projects.size(); i++) {
names[i] = projects.get(i).getRelativeNameFrom(context);
List<String> names = new ArrayList<>();
for (T item: Jenkins.getInstance().allItems(type)) {
names.add(item.getRelativeNameFrom(context));
}
String nearest = EditDistance.findNearest(name, names);
return Jenkins.getInstance().getItem(nearest, context, type);
Expand Down Expand Up @@ -440,6 +508,121 @@ public static <I extends AbstractItem & TopLevelItem> I move(I item, DirectlyMod
return newItem;
}

private static class AllItemsIterable<T extends Item> implements Iterable<T> {

/**
* The authentication we are iterating as.
*/
private final Authentication authentication;
/**
* The root we are iterating from.
*/
private final ItemGroup root;
/**
* The type of item we want to return.
*/
private final Class<T> type;

private AllItemsIterable(ItemGroup root, Authentication authentication, Class<T> type) {
this.root = root;
this.authentication = authentication;
this.type = type;
}

/**
* {@inheritDoc}
*/
@Override
public Iterator<T> iterator() {
return new AllItemsIterator();
}

private class AllItemsIterator implements Iterator<T> {

/**
* The stack of {@link ItemGroup}s that we have left to descend into.
*/
private final Stack<ItemGroup> stack = new Stack<>();
/**
* The iterator of the current {@link ItemGroup} we are iterating.
*/
private Iterator<Item> delegate = null;
/**
* The next item.
*/
private T next = null;

private AllItemsIterator() {
// put on the stack so that hasNext() is the only place that has to worry about authentication
// alternative would be to impersonate and populate delegate.
stack.push(root);
}

/**
* {@inheritDoc}
*/
@Override
public void remove() {
throw new UnsupportedOperationException();
}

/**
* {@inheritDoc}
*/
@Override
public boolean hasNext() {
if (next != null) {
return true;
}
while (true) {
if (delegate == null || !delegate.hasNext()) {
if (stack.isEmpty()) {
return false;
}
ItemGroup group = stack.pop();
Copy link
Member

Choose a reason for hiding this comment

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

I have a concern about the following case:

  • ItemGroup A contains folders D, C, and B
  • C and D are Item groups containing items
  • On the first cycle we go through A => D => (elements of D)
  • Once there is no Elements of D left, the code pops back to the ItemGroup A
  • The code takes the new iterator, so the delegate should get D as a next item again

Maybe I'm reading the code wrong since the test seems to cover this case

Copy link
Member Author

Choose a reason for hiding this comment

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

No it will exhaust the current iteration before popping the stack, not strictly breath first

Copy link
Member Author

Choose a reason for hiding this comment

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

On the first cycle if D C and B are of type T, you will get D, C and B in any order, then it will pop one of those off the stack and process them, A is done when it starts its children

// group.getItems() is responsible for performing the permission check so we will not repeat it
if (Jenkins.getAuthentication() == authentication) {
delegate = group.getItems().iterator();
} else {
// slower path because the caller has switched authentication
// we need to keep the original authentication so that allItems() can be used
// like getAllItems() without the cost of building the entire list up front
try (ACLContext ctx = ACL.as(authentication)) {
Copy link
Member

Choose a reason for hiding this comment

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

Why don't you impersonate once and only once on the top level of hasNext()? I see not so much reason to potentially reauthenticate it multiple times within the method (depends on the three structure).

Calling the impersonation on the allItems() level could also improve the performance a bit

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the usage pattern may be different. With getAllItems() you get the list using current auth, you can then switch auth and do whatever you need...

With this, if you change auth after getting the Iterable it will behave the same as getting the List. Without the impersonation, we have an unpredictable iterable.

The fast case where you just use the same auth throughout will not impersonate.

for (Item i: Items.allItems(Jenkins.getInstance(), Item.class)) {
  doSomething(i); // has side-effect of changing auth
}

Now the side-effect may be legitimate, eg authorise project related... or it may be a bug...

But if we don't guard against it then it is not safe to replace getAllItems with allItems.

The slow part will be the call to getItems() as that does the permission checks, impersonation around that should be relatively cheap

Copy link
Member

Choose a reason for hiding this comment

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

I do not feel it exactly answers my questions, but I agree the permission checks will be more expensive for non-System users. So it's rather NIT

Copy link
Member Author

Choose a reason for hiding this comment

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

For 99.9% of use cases, people will not be changing authentication while iterating, and hence

if (Jenkins.getAuthentication() == authentication) {
    delegate = group.getItems().iterator();
}

Will be the path taken... which will just be inlined to a call to SecurityContextHolder.getContext().getAuthentication()... given that the getItems() will be making this call anyway, the only side-effect in terms of performance is that we will have pre-seeded the CPU cache with the object chain for the SecurityContextHolder.getContext().getAuthentication() call anyway.

There are some cases where the authentication used to enumerate the items does not match to the current authentication.

In Core there is one example: https://github.com/jenkinsci/jenkins/pull/2665/files#diff-46dd855f38f94f25c68cbdae7e6ba64bL243 the example is correct:

  • User decides to rename / move a job
  • The operation runs as the user's permission
  • The item's renameTo does escalation to ACL.SYSTEM for the parts that require this escalation, but otherwise is running as the user who did the rename.
  • Thus, while iterating the iterable, the items should be the items visible to ACL.SYSTEM but the current authentication is not ACL.SYSTEM...

The second half of the branch is to cover this exact case.

HTH

delegate = group.getItems().iterator();
}
}
}
while (delegate.hasNext()) {
Item item = delegate.next();
if (item instanceof ItemGroup) {
stack.push((ItemGroup) item);
}
if (type.isInstance(item)) {
next = type.cast(item);
return true;
}
}
}
}

/**
* {@inheritDoc}
*/
@Override
public T next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
try {
return next;
} finally {
next = null;
}
}

}
}

/**
* Used to load/save job configuration.
*
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/java/hudson/model/Label.java
Expand Up @@ -362,10 +362,11 @@ private String toString(Collection<? extends ModelObject> model) {
@Exported
public List<AbstractProject> getTiedJobs() {
List<AbstractProject> r = new ArrayList<AbstractProject>();
for (AbstractProject<?,?> p : Jenkins.getInstance().getAllItems(AbstractProject.class)) {
for (AbstractProject<?,?> p : Jenkins.getInstance().allItems(AbstractProject.class)) {
if(p instanceof TopLevelItem && this.equals(p.getAssignedLabel()))
r.add(p);
}
Collections.sort(r, Items.BY_FULL_NAME);
return r;
}

Expand Down
4 changes: 2 additions & 2 deletions core/src/main/java/hudson/model/ListView.java
Expand Up @@ -480,7 +480,7 @@ private void locationChanged(Item item, String oldFullName, String newFullName)
renameViewItem(oldFullName, newFullName, jenkins, (ListView) view);
}
}
for (Item g : jenkins.getAllItems()) {
for (Item g : jenkins.allItems()) {
if (g instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) g;
for (View v : vg.getViews()) {
Expand Down Expand Up @@ -524,7 +524,7 @@ private void deleted(Item item) {
deleteViewItem(item, jenkins, (ListView) view);
}
}
for (Item g : jenkins.getAllItems()) {
for (Item g : jenkins.allItems()) {
if (g instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) g;
for (View v : vg.getViews()) {
Expand Down
22 changes: 15 additions & 7 deletions core/src/main/java/hudson/model/UsageStatistics.java
Expand Up @@ -158,14 +158,22 @@ public String getStatData() throws IOException {
o.put("plugins",plugins);

JSONObject jobs = new JSONObject();
List<TopLevelItem> items = j.getAllItems(TopLevelItem.class);
for (TopLevelItemDescriptor d : Items.all()) {
int cnt=0;
for (TopLevelItem item : items) {
if(item.getDescriptor()==d)
cnt++;
// capture the descriptors as these should be small compared with the number of items
// so we will walk all items only once and we can short-cut the search of descriptors
TopLevelItemDescriptor[] descriptors = Items.all().toArray(new TopLevelItemDescriptor[0]);
int counts[] = new int[descriptors.length];
for (TopLevelItem item: j.allItems(TopLevelItem.class)) {
TopLevelItemDescriptor d = item.getDescriptor();
for (int i = 0; i < descriptors.length; i++) {
if (d == descriptors[i]) {
counts[i]++;
// no point checking any more, we found the match
break;
}
}
jobs.put(d.getJsonSafeClassName(),cnt);
}
for (int i = 0; i < descriptors.length; i++) {
jobs.put(descriptors[i].getJsonSafeClassName(), counts[i]);
}
o.put("jobs",jobs);

Expand Down