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

morebits: add Morebits.taskManager #1036

Merged
merged 3 commits into from
Oct 18, 2020

Conversation

siddharthvp
Copy link
Member

New task manager class (first mentioned in #839 (comment)) to automatically determine the execution sequence of asynchronous functions and run them, given the list of tasks (functions returning promises) and their dependencies.

I'll try to incorporate this into the xfd module at the first opportunity. But how to proceed is a bit unclear at the moment due to conflicts posed by #911 and #1030 (#911 isn't necessary to promisify the code in xfd module, it can also be done just putting var def = $.Deferred() at the top of each function and returning it).

@siddharthvp
Copy link
Member Author

Unit test for this available at https://gist.github.com/siddharthvp/8796b9f44a95a3d2fbaf2bb437397706.

The version used there also has some console logging so that you can see what is executing and when. The functions just contain a setTimeout that resolve after some period. By the way the names of the functions and the dependencies are from a real example.

This rather atypical kind of code, so I'm happy to answer any questions.

@siddharthvp
Copy link
Member Author

Well, I see that are no current PRs for the prod module. So I went ahead and overhauled PROD to use Morebits.taskManager. Excited to note that this went smoothly.

This has been extensively tested. Would be great to see this merged soon-ish.

Quoting from the commit message:

No actual changes made to the code, other than moving things around and making the functions return a $.Deferred() object.

Issues resolved:

  • The code is now MUCH more easy to follow. All functionality is broken up into functions whose names say exactly what the function does, unlike before when we had a function called main() that does half of the things itself.
    • Until now we were having lot of lines like pageobj.setCallbackParamters(params) or var params = pageobj.getCallbackParameters(), or in case of api objects wikipedia_api.params = params or var params = apiobj.params. The same object (not a copy of the object) was being passed around in every function. In effect, the object was a global. What I have done now is to put an end to the mishegoss and actually declare it as a global accessible from anywhere without unnecessary paperwork.
  • Fixes Don't post {{Old prod}} if prod tagging fails #726. {{Old prod}} is now only posted if the tagging was successful.
  • Fixes the unreported issue of author notification succeeding even if tagging was aborted (such as when there was already a prod tag, and the user chose not to endorse).

@siddharthvp
Copy link
Member Author

This is now also in use on User:SD0001/GAR-helper.js as another example usage.

@siddharthvp
Copy link
Member Author

I've decided to do away with the function that checks for circular dependencies. I grossly over-estimated the amount of complexity of the call chains we'd be having. It turns out that they are simple enough for a human to easily tell if there are any circular links without needing to use automation.

Reproducing the code here, should it of interest:

/**
 * Check if there are any cyclic dependencies.
 * We represent the tasks as the vertices of a directed graph whose edges
 * represent dependencies, then perform a depth-first traversal.
 * See [[Depth-first search]] for a background on how this works.
 *
 * XXX: Is this worth it?
 * Useful for testing and debugging but shouldn't be required in production code
 */
this.checkCircularDependencies = function() {
	var visited = new Set();
	var executed = new Set();
	var dfs = function(task) {
		visited.add(task);
		var circularLink;
		var deps = this.taskDependencyMap.get(task);
		for (var i = 0; i < deps.length; i++) {
			if (!visited.has(deps[i])) {
				if (circularLink = dfs(deps[i])) { // eslint-disable-line no-cond-assign
					return circularLink;
				}
			} else if (!executed.has(deps[i])) {
				return deps[i];
			}
		}
		executed.add(task);
	}.bind(this);
	this.taskDependencyMap.forEach(function(_, task) {
		var link;
		if (!visited.has(task)) {
			if (link = dfs(task)) { // eslint-disable-line no-cond-assign
				// link.name (which gives the name of the function) won't work in IE 11,
				// so you're on your own in finding the circular link
				throw new Error('circular dependency detected at ' + link.name);
			}
		}
	});
};

@siddharthvp
Copy link
Member Author

Also added support for resolving tasks with value (which would be available as arguments for use by the dependent functions). Updated the unit tests.

@Amorymeltzer Amorymeltzer force-pushed the taskmanager branch 3 times, most recently from 31640d1 to ea27468 Compare August 13, 2020 11:53
Copy link
Collaborator

@Amorymeltzer Amorymeltzer left a comment

Choose a reason for hiding this comment

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

This seems nice! I think it's actually a pretty big change, if subtly so, but it's nifty.

Aside from the redirect location issue, one other issue is the timing of that redirection. The timer seems to kick off from the start, so if there's any hangup (slow internet/computer, waiting for confirmation) then the page redirects immediately, possibly before the final action visually updates. I haven't been able to trigger anything actually not going through, but prod is fairly simple so I'd worry about something like AfD. At the very least, there's minimal chance for review.

* @param {function[]} deps - its dependencies
*/
this.add = function(func, deps) {
this.taskDependencyMap.set(func, deps);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Seems silly to require tm.add(func, []), is there a reason not to default to empty?

Suggested change
this.taskDependencyMap.set(func, deps);
deps = deps || [];
this.taskDependencyMap.set(func, deps);

Copy link
Member Author

Choose a reason for hiding this comment

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

Wouldn't be better to make the empty list explicit? In any case, I won't be able to push any code changes for quite some time.

Copy link
Collaborator

@Amorymeltzer Amorymeltzer Aug 13, 2020

Choose a reason for hiding this comment

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

It feels natural to me to not have to provide anything if there're no dependencies. Clearly not the most critical concern!

'prop': 'templates',
'tltemplates': blocking_templates
};
Morebits.wiki.actionCompleted.redirect = mw.config.get('wgPageName');
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not entirely sure where/why, but this is all messed up? I'm getting redirected from /wiki/PageName to /wiki//wiki/PageName

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually, that's only when not notifying; when notifying, it adds another, redirecting to /wiki//wiki//wiki/PageName

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 don't recall coming across this in my testing. Morebits.wiki.actionCompleted.* is a messy system (basically depends on race conditions) which we probably don't want to use at least in places where taskManager is being used. I would suggest putting

location.href = mw.util.getUrl(  mw.config.get('wgPageName') ); 

in the execute's then block. This method should be more reliable and clearer.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I haven't had much time so have been adding/tweaking a bunch of jsdoc stuff since I can do it in 30 second chunks in the morning and, wow, actionCompleted is entirely inscrutable to me. I can try and take a look in September if you don't have a chance, but, oof, there be dragons.

@@ -174,205 +174,231 @@ Twinkle.prod.callback.prodtypechanged = function(event) {
event.target.form.replaceChild(field.render(), $(event.target.form).find('fieldset[name="parameters"]')[0]);
};

// global params object, initially set in evaluate(), and
// modified in various callback functions
var params = {};
Copy link
Collaborator

@Amorymeltzer Amorymeltzer Aug 13, 2020

Choose a reason for hiding this comment

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

Sorry for all the pushes, I missed this initially. I suppose it's okay here, but I think we'd want to be careful about expanding this elsewhere. The good news is it's how I found #1102!

@Amorymeltzer Amorymeltzer added this to the October 2020 update milestone Sep 5, 2020
@siddharthvp siddharthvp mentioned this pull request Oct 9, 2020
Amorymeltzer added a commit to Amorymeltzer/twinkle that referenced this pull request Oct 9, 2020
Since wikimedia-gadgets#1093 (7a450cb), in an attempt to avoid pointless errors leading to *actual* errors, I moved the `pageobj.triage()` call to after the `Twinkle.xfd.currentRationale = null` limits such errors.  This, however, meant that since then, AfD nominations were always trying to triage the *nomination* page, not the *nominated* page.  This fixes that, although like wikimedia-gadgets#1093, in a somewhat kludgy way; anywhere else and we risk the page nomination resolving first (wikimedia-gadgets#1036).  Increasingly thinking the original version of wikimedia-gadgets#930 would be the cleaner way to handle this.
@Amorymeltzer Amorymeltzer removed this from the October 2020 update milestone Oct 17, 2020
No acutal changes made to the code, other than moving things around and making the functions return a $.Deferred() object.

Issues resolved:
- The code is now MUCH more easy to follow. All functionality is broken up into functions whose names say exactly what the function does, unlike before when we had a function called main() that does half of the things itself.
  - Until now we were having lot of lines like `pageobj.setCallbackParamters(params)` or `var params = pageobj.getCallbackParameters()`, or in case of api objects `wikipedia_api.params = params` or `var params = apiobj.params`. The same object (not a copy of the object) was being passed around in every function. In effect, the object was a global. What I have done now is to put an end to the mishegoss and actually declare it as a global accessible from anywhere without unnecessary paperwork.
- Fixes wikimedia-gadgets#726. {{Old prod}} is now only posted if the tagging was successful.
- Fixes the unreported issue of author notification succeeding even if tagging was aborted (such as when there was already a prod tag, and the user chose not to endorse).
in favour of doing the redirection in the then callback of execute().

Also ensure taggingPage() is done before doing addOldProd(), to avoid
adding {{old prod}} if tagging was not done in the first place.
@siddharthvp
Copy link
Member Author

Fixed the issue (by not using Morebits.wiki.actionCompleted setup). All seems good now.

@siddharthvp siddharthvp merged commit 01fc94c into wikimedia-gadgets:master Oct 18, 2020
@siddharthvp siddharthvp deleted the taskmanager branch October 18, 2020 05:38
@Amorymeltzer Amorymeltzer added this to the October 2020 update milestone Oct 18, 2020
@Amorymeltzer
Copy link
Collaborator

As above, this seems fine, and the taskManager is neat, but there are some things I don't think should be duplicated excessively, especially the subtle deprecation of Morebits.wiki.actionCompleted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants