Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions queue_job/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,9 @@ Features:
- Views for jobs, jobs are stored in PostgreSQL
- Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's
NOTIFY
- Channels: give a capacity for the root channel and its sub-channels
and segregate jobs in them. Allow for instance to restrict heavy jobs
to be executed one at a time while little ones are executed 4 at a
times.
- Channels: give a capacity for the root channel and its subchannels and
segregate jobs in them. Allow for instance to restrict heavy jobs to
be executed one at a time while little ones are executed 4 at a times.
- Retries: Ability to retry jobs by raising a type of exception
- Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next
tries, retry after 1 minutes, ...
Expand Down Expand Up @@ -193,7 +192,7 @@ The execution of channels by the job runner is defined by:
- ``sequential``: jobs run one after the other, and a failed job blocks
the channel (requires a capacity of 1)
- ``throttle``: minimum delay, in seconds, between the start of two jobs
- ``paused``: stop running jobs in this channel and its sub-channels
- ``paused``: stop running jobs in this channel and its subchannels

**Job Runner Configuration Parameters**

Expand Down
1 change: 1 addition & 0 deletions queue_job/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"assets": {
"web.assets_backend": [
"/queue_job/static/src/views/**/*",
"/queue_job/static/src/channel_tree/**/*",
],
},
"installable": True,
Expand Down
49 changes: 45 additions & 4 deletions queue_job/models/queue_job_channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from odoo import _, api, exceptions, fields, models

from ..jobrunner import runner
from ..jobrunner.channels import RELOAD_PAYLOAD


Expand Down Expand Up @@ -57,15 +58,29 @@ class QueueJobChannel(models.Model):
help="Minimum delay in seconds between the start of two jobs in this channel."
)
paused = fields.Boolean(
help="A paused channel (an its sub-channels) do not execute any jobs until "
help="A paused channel (an its subchannels) do not execute any jobs until "
"resumed."
)
capacity_default = fields.Integer(
help="Default capacity for unconfigured sub-channels. "
"0 means they would have the same capacity as the current channel."
string="Subchannels Default Capacity",
help="Default capacity for unconfigured subchannels. "
"0 means they would have the same capacity as the current channel.",
)
sequential_default = fields.Boolean(
help="If sequential is enabled for unconfigured sub-channels."
string="Subchannels Default Sequential",
help="If sequential is enabled for unconfigured subchannels.",
)
effective_paused = fields.Boolean(
compute="_compute_effective_paused",
recursive=True,
help="If this channel is actually paused, depending on the parents.",
)
effective_capacity = fields.Integer(
compute="_compute_effective_capacity",
recursive=True,
help="Actual capacity of this channel. Its own capacity if set, "
"or the capacity of the closest parent. The root channel "
"is limited by the server-wide max_capacity/db_max_capacity configuration.",
)

_sql_constraints = [
Expand Down Expand Up @@ -112,6 +127,32 @@ def _compute_complete_name(self):
complete_name = record.name
record.complete_name = complete_name

@api.depends("paused", "parent_id.effective_paused")
def _compute_effective_paused(self):
for record in self:
record.effective_paused = record.paused or bool(
record.parent_id and record.parent_id.effective_paused
)

@api.depends("capacity", "parent_id.effective_capacity")
def _compute_effective_capacity(self):
for record in self:
if record.parent_id:
max_capacity = record.parent_id.effective_capacity
else:
max_capacity = record._root_max_capacity()
if record.capacity:
record.effective_capacity = min(record.capacity, max_capacity)
else:
record.effective_capacity = max_capacity

def _root_max_capacity(self):
"""Server-wide capacity of the root channel for current database."""
rules = runner.parse_db_max_capacity(runner._db_max_capacity())
return runner.db_max_capacity_for(
self.env.cr.dbname, rules, default=runner._max_capacity()
)

@api.constrains("parent_id", "name")
def parent_required(self):
for record in self:
Expand Down
2 changes: 1 addition & 1 deletion queue_job/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The execution of channels by the job runner is defined by:
- `sequential`: jobs run one after the other, and a failed job blocks the
channel (requires a capacity of 1)
- `throttle`: minimum delay, in seconds, between the start of two jobs
- `paused`: stop running jobs in this channel and its sub-channels
- `paused`: stop running jobs in this channel and its subchannels


**Job Runner Configuration Parameters**
Expand Down
2 changes: 1 addition & 1 deletion queue_job/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Features:
- Views for jobs, jobs are stored in PostgreSQL
- Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL's
NOTIFY
- Channels: give a capacity for the root channel and its sub-channels
- Channels: give a capacity for the root channel and its subchannels
and segregate jobs in them. Allow for instance to restrict heavy jobs
to be executed one at a time while little ones are executed 4 at a
times.
Expand Down
9 changes: 4 additions & 5 deletions queue_job/static/description/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -405,10 +405,9 @@ <h1>Job Queue</h1>
<li>Views for jobs, jobs are stored in PostgreSQL</li>
<li>Jobrunner: execute the jobs, highly efficient thanks to PostgreSQL’s
NOTIFY</li>
<li>Channels: give a capacity for the root channel and its sub-channels
and segregate jobs in them. Allow for instance to restrict heavy jobs
to be executed one at a time while little ones are executed 4 at a
times.</li>
<li>Channels: give a capacity for the root channel and its subchannels and
segregate jobs in them. Allow for instance to restrict heavy jobs to
be executed one at a time while little ones are executed 4 at a times.</li>
<li>Retries: Ability to retry jobs by raising a type of exception</li>
<li>Retry Pattern: the 3 first tries, retry after 10 seconds, the 5 next
tries, retry after 1 minutes, …</li>
Expand Down Expand Up @@ -540,7 +539,7 @@ <h2><a class="toc-backref" href="#toc-entry-3">Configuration</a></h2>
<li><tt class="docutils literal">sequential</tt>: jobs run one after the other, and a failed job blocks
the channel (requires a capacity of 1)</li>
<li><tt class="docutils literal">throttle</tt>: minimum delay, in seconds, between the start of two jobs</li>
<li><tt class="docutils literal">paused</tt>: stop running jobs in this channel and its sub-channels</li>
<li><tt class="docutils literal">paused</tt>: stop running jobs in this channel and its subchannels</li>
</ul>
<p><strong>Job Runner Configuration Parameters</strong></p>
<ul class="simple">
Expand Down
226 changes: 226 additions & 0 deletions queue_job/static/src/channel_tree/channel_tree.esm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
/* @odoo-module */
/* global vis */

import {loadCSS, loadJS} from "@web/core/assets";

import {_t} from "@web/core/l10n/translation";
import {registry} from "@web/core/registry";
import {useService} from "@web/core/utils/hooks";

import weUtils from "@web_editor/js/common/utils";

const {Component, onWillStart, useRef, onMounted} = owl;

const {document} = globalThis;

const SERVER_NODE = "server_node";

const CHANNEL_FIELDS = [
"name",
"complete_name",
"parent_id",
"capacity",
"effective_capacity",
"sequential",
"throttle",
"paused",
"effective_paused",
"capacity_default",
"sequential_default",
];

class ChannelTree extends Component {
static template = "queue_job.ChannelTree";
static props = ["*"];

setup() {
this.orm = useService("orm");
this.action = useService("action");
this.rootRef = useRef("root_vis");
this.network = null;

onWillStart(async () => {
await Promise.all([
loadJS("/queue_job/static/lib/vis/vis-network.min.js"),
loadCSS("/queue_job/static/lib/vis/vis-network.min.css"),
]);
await this.loadChannels();
});

onMounted(() => this.renderNetwork());
}

get $el() {
return this.rootRef.el;
}

async loadChannels() {
this.channels = await this.orm.searchRead(
"queue.job.channel",
[],
CHANNEL_FIELDS
);
}

htmlTitle(html) {
const container = document.createElement("div");
container.innerHTML = html;
return container;
}

edgeWidth(capacity) {
if (!capacity) {
return 1;
}
return capacity;
}

nodePopup(channel) {
const rows = [
[_t("Capacity"), channel.capacity || _t("inherited")],
[_t("Effective capacity"), channel.effective_capacity],
[_t("Sequential"), channel.sequential ? _t("yes") : _t("no")],
[
_t("Throttle"),
channel.throttle ? `${channel.throttle}${_t("s")}` : _t("none"),
],
[_t("Default capacity"), channel.capacity_default || _t("inherited")],
[
_t("Default sequential"),
channel.sequential_default ? _t("yes") : _t("no"),
],
];

let pausedStatus = "";
if (channel.paused) {
pausedStatus = ` (${_t("Paused")})`;
} else if (channel.effective_paused) {
pausedStatus = ` (${_t("Paused by parent")})`;
}
const headerTitle = `${channel.complete_name}${pausedStatus}`;
const header = `<div style="margin-bottom: 6px;"><b>${headerTitle}</b></div>`;

const lines = rows
.map(([label, value]) => `<div><b>${label}:</b> ${value}</div>`)
.join("");

const hints =
`<div class="text-muted small" style="margin-top: 6px;">` +
`<div>${_t("Double-click to open the channel")}</div>` +
`<div>${_t("Right-click to create a subchannel")}</div>` +
`</div>`;
return `<div>${header}${lines}${hints}</div>`;
}

renderNetwork() {
if (this.network) {
this.$el.innerHTML = "";
}

const activeColor = weUtils.getCSSVariableValue("teal");
const pausedColor = weUtils.getCSSVariableValue("orange");

const nodes = this.channels.map((channel) => ({
id: channel.id,
label: channel.name,
title: this.htmlTitle(this.nodePopup(channel)),
color: channel.effective_paused ? pausedColor : activeColor,
// Show root channel larger
font: channel.parent_id ? undefined : {size: 20},
}));

const edges = this.channels
.filter((channel) => channel.parent_id)
.map((channel) => ({
from: channel.parent_id[0],
to: channel.id,
width: this.edgeWidth(channel.effective_capacity),
label: String(channel.effective_capacity),
title: _t("Effective capacity: %s", channel.effective_capacity),
}));

const rootChannel = this.channels.find((channel) => !channel.parent_id);
if (rootChannel) {
// Build an invisible node on the left of the root channel to
// show an outgoing line towards "an exit" (execution by the server)
nodes.push({
id: SERVER_NODE,
label: "",
color: {background: "transparent", border: "transparent"},
chosen: false,
});
edges.push({
from: SERVER_NODE,
to: rootChannel.id,
width: this.edgeWidth(rootChannel.effective_capacity),
label: String(rootChannel.effective_capacity),
title: _t("Server capacity: %s", rootChannel.effective_capacity),
// The line would be transparent (as the node itself) otherwise
color: {color: activeColor, inherit: false},
});
}

const data = {
nodes: new vis.DataSet(nodes),
edges: new vis.DataSet(edges),
};

const options = {
layout: {
hierarchical: {
direction: "LR",
sortMethod: "directed",
// Align each channel "level" with the same level (default
// is to align leaves on the right (e.g. with root.p1 and
// root.p2.foo, shakeTowards: roots aligns p1 with p2,
// whereas the defaults aligns p1 with foo)
shakeTowards: "roots",
},
},
physics: false,
};

const network = new vis.Network(this.$el, data, options);
network.on("doubleClick", (params) => {
if (params.nodes.length > 0 && params.nodes[0] !== SERVER_NODE) {
this.actionOpenChannel(params.nodes[0]);
}
});
network.on("oncontext", (params) => {
params.event.preventDefault();
const nodeId = network.getNodeAt(params.pointer.DOM);
if (nodeId !== undefined && nodeId !== SERVER_NODE) {
this.actionNewChildChannel(nodeId);
}
});

this.network = network;
}

async actionOpenChannel(resId) {
const action = await this.orm.call("queue.job.channel", "get_formview_action", [
[resId],
]);
await this.action.doAction(action);
}

async actionNewChildChannel(parentId) {
await this.action.doAction(
{
type: "ir.actions.act_window",
res_model: "queue.job.channel",
views: [[false, "form"]],
target: "new",
context: {default_parent_id: parentId},
},
{
onClose: async () => {
await this.loadChannels();
this.renderNetwork();
},
}
);
}
}

registry.category("actions").add("queue_job_channel_tree", ChannelTree);
Loading
Loading