mirror of
https://github.com/ansible/awx.git
synced 2026-03-04 18:21:03 -03:30
Merge pull request #10053 from AlanCoding/dropsies
Intentionally drop job event websocket messages in excess of 30 per second (configurable) SUMMARY The UI no longer follows the latest job events from websocket messages. Because of that, there's no reason to send messages for all events if the job event rate is high. I used 30 because this is the number of events that I guesstimate will show in one page in the UI. Needs the setting added in the UI. This adds skip_websocket_message to event event_data. We could promote it to a top-level key for job events, if that is preferable aesthetically. Doing this allows us to test this feature without having to connect a websocket client. Ping @mabashian @chrismeyersfsu ISSUE TYPE Feature Pull Request COMPONENT NAME API UI ADDITIONAL INFORMATION Scenario walkthrough: a job is producing 1,000 events per second. User launches it, the screen fills up in, say 1/4 of a second. The scrollbar indicates content beyond the bottom of the screen. Now, for 3/4ths of a second, the scrollbar stays still. After that, it updates the scrollbar to the current line number that the job is on. The scrollbar continues to update the length of the output effectively once per second. Reviewed-by: Alan Rominger <arominge@redhat.com> Reviewed-by: Chris Meyers <None> Reviewed-by: Jake McDermott <yo@jakemcdermott.me>
This commit is contained in:
@@ -362,6 +362,17 @@ register(
|
|||||||
category_slug='jobs',
|
category_slug='jobs',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
register(
|
||||||
|
'MAX_WEBSOCKET_EVENT_RATE',
|
||||||
|
field_class=fields.IntegerField,
|
||||||
|
min_value=0,
|
||||||
|
default=30,
|
||||||
|
label=_('Job Event Maximum Websocket Messages Per Second'),
|
||||||
|
help_text=_('Maximum number of messages to update the UI live job output with per second. Value of 0 means no limit.'),
|
||||||
|
category=_('Jobs'),
|
||||||
|
category_slug='jobs',
|
||||||
|
)
|
||||||
|
|
||||||
register(
|
register(
|
||||||
'SCHEDULE_MAX_JOBS',
|
'SCHEDULE_MAX_JOBS',
|
||||||
field_class=fields.IntegerField,
|
field_class=fields.IntegerField,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ STANDARD_INVENTORY_UPDATE_ENV = {
|
|||||||
}
|
}
|
||||||
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
CAN_CANCEL = ('new', 'pending', 'waiting', 'running')
|
||||||
ACTIVE_STATES = CAN_CANCEL
|
ACTIVE_STATES = CAN_CANCEL
|
||||||
|
MINIMAL_EVENTS = set(['playbook_on_play_start', 'playbook_on_task_start', 'playbook_on_stats', 'EOF'])
|
||||||
CENSOR_VALUE = '************'
|
CENSOR_VALUE = '************'
|
||||||
ENV_BLOCKLIST = frozenset(
|
ENV_BLOCKLIST = frozenset(
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
logger.exception('Database Error Saving Job Event')
|
logger.exception('Database Error Saving Job Event')
|
||||||
duration_to_save = time.perf_counter() - duration_to_save
|
duration_to_save = time.perf_counter() - duration_to_save
|
||||||
for e in events:
|
for e in events:
|
||||||
emit_event_detail(e)
|
if not getattr(e, '_skip_websocket_message', False):
|
||||||
|
emit_event_detail(e)
|
||||||
self.buff = {}
|
self.buff = {}
|
||||||
self.last_flush = time.time()
|
self.last_flush = time.time()
|
||||||
# only update metrics if we saved events
|
# only update metrics if we saved events
|
||||||
@@ -207,7 +208,13 @@ class CallbackBrokerWorker(BaseWorker):
|
|||||||
GuidMiddleware.set_guid('')
|
GuidMiddleware.set_guid('')
|
||||||
return
|
return
|
||||||
|
|
||||||
|
skip_websocket_message = body.pop('skip_websocket_message', False)
|
||||||
|
|
||||||
event = cls.create_from_data(**body)
|
event = cls.create_from_data(**body)
|
||||||
|
|
||||||
|
if skip_websocket_message:
|
||||||
|
event._skip_websocket_message = True
|
||||||
|
|
||||||
self.buff.setdefault(cls, []).append(event)
|
self.buff.setdefault(cls, []).append(event)
|
||||||
|
|
||||||
retries = 0
|
retries = 0
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from awx.api.versioning import reverse
|
|||||||
from awx.main import consumers
|
from awx.main import consumers
|
||||||
from awx.main.managers import DeferJobCreatedManager
|
from awx.main.managers import DeferJobCreatedManager
|
||||||
from awx.main.fields import JSONField
|
from awx.main.fields import JSONField
|
||||||
|
from awx.main.constants import MINIMAL_EVENTS
|
||||||
from awx.main.models.base import CreatedModifiedModel
|
from awx.main.models.base import CreatedModifiedModel
|
||||||
from awx.main.utils import ignore_inventory_computed_fields, camelcase_to_underscore
|
from awx.main.utils import ignore_inventory_computed_fields, camelcase_to_underscore
|
||||||
|
|
||||||
@@ -57,9 +58,6 @@ def create_host_status_counts(event_data):
|
|||||||
return dict(host_status_counts)
|
return dict(host_status_counts)
|
||||||
|
|
||||||
|
|
||||||
MINIMAL_EVENTS = set(['playbook_on_play_start', 'playbook_on_task_start', 'playbook_on_stats', 'EOF'])
|
|
||||||
|
|
||||||
|
|
||||||
def emit_event_detail(event):
|
def emit_event_detail(event):
|
||||||
if settings.UI_LIVE_UPDATES_ENABLED is False and event.event not in MINIMAL_EVENTS:
|
if settings.UI_LIVE_UPDATES_ENABLED is False and event.event not in MINIMAL_EVENTS:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
|
|
||||||
# Python
|
# Python
|
||||||
from collections import OrderedDict, namedtuple
|
from collections import OrderedDict, namedtuple, deque
|
||||||
import errno
|
import errno
|
||||||
import functools
|
import functools
|
||||||
import importlib
|
import importlib
|
||||||
@@ -57,7 +57,7 @@ from receptorctl.socket_interface import ReceptorControl
|
|||||||
|
|
||||||
# AWX
|
# AWX
|
||||||
from awx import __version__ as awx_application_version
|
from awx import __version__ as awx_application_version
|
||||||
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV
|
from awx.main.constants import PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV, MINIMAL_EVENTS
|
||||||
from awx.main.access import access_registry
|
from awx.main.access import access_registry
|
||||||
from awx.main.redact import UriCleaner
|
from awx.main.redact import UriCleaner
|
||||||
from awx.main.models import (
|
from awx.main.models import (
|
||||||
@@ -740,6 +740,7 @@ class BaseTask(object):
|
|||||||
self.host_map = {}
|
self.host_map = {}
|
||||||
self.guid = GuidMiddleware.get_guid()
|
self.guid = GuidMiddleware.get_guid()
|
||||||
self.job_created = None
|
self.job_created = None
|
||||||
|
self.recent_event_timings = deque(maxlen=settings.MAX_WEBSOCKET_EVENT_RATE)
|
||||||
|
|
||||||
def update_model(self, pk, _attempt=0, **updates):
|
def update_model(self, pk, _attempt=0, **updates):
|
||||||
"""Reload the model instance from the database and update the
|
"""Reload the model instance from the database and update the
|
||||||
@@ -1151,6 +1152,37 @@ class BaseTask(object):
|
|||||||
if 'event_data' in event_data:
|
if 'event_data' in event_data:
|
||||||
event_data['event_data']['guid'] = self.guid
|
event_data['event_data']['guid'] = self.guid
|
||||||
|
|
||||||
|
# To prevent overwhelming the broadcast queue, skip some websocket messages
|
||||||
|
if self.recent_event_timings:
|
||||||
|
cpu_time = time.time()
|
||||||
|
first_window_time = self.recent_event_timings[0]
|
||||||
|
last_window_time = self.recent_event_timings[-1]
|
||||||
|
|
||||||
|
if event_data.get('event') in MINIMAL_EVENTS:
|
||||||
|
should_emit = True # always send some types like playbook_on_stats
|
||||||
|
elif event_data.get('stdout') == '' and event_data['start_line'] == event_data['end_line']:
|
||||||
|
should_emit = False # exclude events with no output
|
||||||
|
else:
|
||||||
|
should_emit = any(
|
||||||
|
[
|
||||||
|
# if 30the most recent websocket message was sent over 1 second ago
|
||||||
|
cpu_time - first_window_time > 1.0,
|
||||||
|
# if the very last websocket message came in over 1/30 seconds ago
|
||||||
|
self.recent_event_timings.maxlen * (cpu_time - last_window_time) > 1.0,
|
||||||
|
# if the queue is not yet full
|
||||||
|
len(self.recent_event_timings) != self.recent_event_timings.maxlen,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_emit:
|
||||||
|
self.recent_event_timings.append(cpu_time)
|
||||||
|
else:
|
||||||
|
event_data.setdefault('event_data', {})
|
||||||
|
event_data['skip_websocket_message'] = True
|
||||||
|
|
||||||
|
elif self.recent_event_timings.maxlen:
|
||||||
|
self.recent_event_timings.append(time.time())
|
||||||
|
|
||||||
event_data.setdefault(self.event_data_key, self.instance.id)
|
event_data.setdefault(self.event_data_key, self.instance.id)
|
||||||
self.dispatcher.dispatch(event_data)
|
self.dispatcher.dispatch(event_data)
|
||||||
self.event_ct += 1
|
self.event_ct += 1
|
||||||
|
|||||||
@@ -197,8 +197,9 @@ UI_LIVE_UPDATES_ENABLED = True
|
|||||||
# beyond this limit and the value will be removed
|
# beyond this limit and the value will be removed
|
||||||
MAX_EVENT_RES_DATA = 700000
|
MAX_EVENT_RES_DATA = 700000
|
||||||
|
|
||||||
# Note: This setting may be overridden by database settings.
|
# Note: These settings may be overridden by database settings.
|
||||||
EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024
|
EVENT_STDOUT_MAX_BYTES_DISPLAY = 1024
|
||||||
|
MAX_WEBSOCKET_EVENT_RATE = 30
|
||||||
|
|
||||||
# The amount of time before a stdout file is expired and removed locally
|
# The amount of time before a stdout file is expired and removed locally
|
||||||
# Note that this can be recreated if the stdout is downloaded
|
# Note that this can be recreated if the stdout is downloaded
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { HostStatusBar, OutputToolbar } from './shared';
|
|||||||
import getRowRangePageSize from './shared/jobOutputUtils';
|
import getRowRangePageSize from './shared/jobOutputUtils';
|
||||||
import { getJobModel, isJobRunning } from '../../../util/jobs';
|
import { getJobModel, isJobRunning } from '../../../util/jobs';
|
||||||
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
import useRequest, { useDismissableError } from '../../../util/useRequest';
|
||||||
|
import useInterval from '../../../util/useInterval';
|
||||||
import {
|
import {
|
||||||
parseQueryString,
|
parseQueryString,
|
||||||
mergeParams,
|
mergeParams,
|
||||||
@@ -297,8 +298,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
const listRef = useRef(null);
|
const listRef = useRef(null);
|
||||||
const previousWidth = useRef(0);
|
const previousWidth = useRef(0);
|
||||||
const jobSocketCounter = useRef(0);
|
const jobSocketCounter = useRef(0);
|
||||||
const interval = useRef(null);
|
|
||||||
const isMounted = useIsMounted();
|
const isMounted = useIsMounted();
|
||||||
|
const scrollTop = useRef(0);
|
||||||
|
const scrollHeight = useRef(0);
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const [contentError, setContentError] = useState(null);
|
const [contentError, setContentError] = useState(null);
|
||||||
const [cssMap, setCssMap] = useState({});
|
const [cssMap, setCssMap] = useState({});
|
||||||
@@ -310,6 +312,17 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
const [showCancelModal, setShowCancelModal] = useState(false);
|
const [showCancelModal, setShowCancelModal] = useState(false);
|
||||||
const [remoteRowCount, setRemoteRowCount] = useState(0);
|
const [remoteRowCount, setRemoteRowCount] = useState(0);
|
||||||
const [results, setResults] = useState({});
|
const [results, setResults] = useState({});
|
||||||
|
const [isFollowModeEnabled, setIsFollowModeEnabled] = useState(
|
||||||
|
isJobRunning(job.status)
|
||||||
|
);
|
||||||
|
const [isMonitoringWebsocket, setIsMonitoringWebsocket] = useState(false);
|
||||||
|
|
||||||
|
useInterval(
|
||||||
|
() => {
|
||||||
|
monitorJobSocketCounter();
|
||||||
|
},
|
||||||
|
isMonitoringWebsocket ? 5000 : null
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadJobEvents();
|
loadJobEvents();
|
||||||
@@ -330,14 +343,15 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
interval.current = setInterval(() => monitorJobSocketCounter(), 5000);
|
setIsMonitoringWebsocket(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
return function cleanup() {
|
return function cleanup() {
|
||||||
if (ws) {
|
if (ws) {
|
||||||
ws.close();
|
ws.close();
|
||||||
}
|
}
|
||||||
clearInterval(interval.current);
|
setIsMonitoringWebsocket(false);
|
||||||
|
isMounted.current = false;
|
||||||
};
|
};
|
||||||
}, [location.search]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [location.search]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
@@ -347,6 +361,22 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
}
|
}
|
||||||
}, [currentlyLoading, cssMap, remoteRowCount]);
|
}, [currentlyLoading, cssMap, remoteRowCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (jobStatus && !isJobRunning(jobStatus)) {
|
||||||
|
if (jobSocketCounter.current > remoteRowCount && isMounted.current) {
|
||||||
|
setRemoteRowCount(jobSocketCounter.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isMonitoringWebsocket) {
|
||||||
|
setIsMonitoringWebsocket(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFollowModeEnabled) {
|
||||||
|
setTimeout(() => setIsFollowModeEnabled(false), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [jobStatus]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const {
|
const {
|
||||||
error: cancelError,
|
error: cancelError,
|
||||||
isLoading: isCancelling,
|
isLoading: isCancelling,
|
||||||
@@ -381,14 +411,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
} = useDismissableError(deleteError);
|
} = useDismissableError(deleteError);
|
||||||
|
|
||||||
const monitorJobSocketCounter = () => {
|
const monitorJobSocketCounter = () => {
|
||||||
|
if (jobSocketCounter.current > remoteRowCount && isMounted.current) {
|
||||||
|
setRemoteRowCount(jobSocketCounter.current);
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
jobSocketCounter.current === remoteRowCount &&
|
jobSocketCounter.current === remoteRowCount &&
|
||||||
!isJobRunning(job.status)
|
!isJobRunning(job.status)
|
||||||
) {
|
) {
|
||||||
clearInterval(interval.current);
|
setIsMonitoringWebsocket(false);
|
||||||
}
|
|
||||||
if (jobSocketCounter.current > remoteRowCount && isMounted.current) {
|
|
||||||
setRemoteRowCount(jobSocketCounter.current);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -492,6 +522,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const rowRenderer = ({ index, parent, key, style }) => {
|
const rowRenderer = ({ index, parent, key, style }) => {
|
||||||
|
if (listRef.current && isFollowModeEnabled) {
|
||||||
|
setTimeout(() => scrollToRow(remoteRowCount - 1), 0);
|
||||||
|
}
|
||||||
let actualLineTextHtml = [];
|
let actualLineTextHtml = [];
|
||||||
if (results[index]) {
|
if (results[index]) {
|
||||||
const { lineTextHtml } = getLineTextHtml(results[index]);
|
const { lineTextHtml } = getLineTextHtml(results[index]);
|
||||||
@@ -584,7 +617,9 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const scrollToRow = rowIndex => {
|
const scrollToRow = rowIndex => {
|
||||||
listRef.current.scrollToRow(rowIndex);
|
if (listRef.current) {
|
||||||
|
listRef.current.scrollToRow(rowIndex);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleScrollPrevious = () => {
|
const handleScrollPrevious = () => {
|
||||||
@@ -604,7 +639,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleScrollLast = () => {
|
const handleScrollLast = () => {
|
||||||
scrollToRow(remoteRowCount);
|
scrollToRow(remoteRowCount - 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleResize = ({ width }) => {
|
const handleResize = ({ width }) => {
|
||||||
@@ -657,6 +692,27 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
history.push(qs ? `${pathname}?${qs}` : pathname);
|
history.push(qs ? `${pathname}?${qs}` : pathname);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFollowToggle = () => {
|
||||||
|
if (isFollowModeEnabled) {
|
||||||
|
setIsFollowModeEnabled(false);
|
||||||
|
} else {
|
||||||
|
setIsFollowModeEnabled(true);
|
||||||
|
scrollToRow(remoteRowCount - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScroll = e => {
|
||||||
|
if (
|
||||||
|
isFollowModeEnabled &&
|
||||||
|
scrollTop.current > e.scrollTop &&
|
||||||
|
scrollHeight.current === e.scrollHeight
|
||||||
|
) {
|
||||||
|
setIsFollowModeEnabled(false);
|
||||||
|
}
|
||||||
|
scrollTop.current = e.scrollTop;
|
||||||
|
scrollHeight.current = e.scrollHeight;
|
||||||
|
};
|
||||||
|
|
||||||
const renderSearchComponent = () => (
|
const renderSearchComponent = () => (
|
||||||
<Search
|
<Search
|
||||||
qsConfig={QS_CONFIG}
|
qsConfig={QS_CONFIG}
|
||||||
@@ -763,6 +819,14 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
)}
|
)}
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</ToolbarToggleGroup>
|
</ToolbarToggleGroup>
|
||||||
|
{isJobRunning(job.status) ? (
|
||||||
|
<Button
|
||||||
|
variant={isFollowModeEnabled ? 'secondary' : 'primary'}
|
||||||
|
onClick={handleFollowToggle}
|
||||||
|
>
|
||||||
|
{isFollowModeEnabled ? t`Unfollow` : t`Follow`}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</SearchToolbarContent>
|
</SearchToolbarContent>
|
||||||
</SearchToolbar>
|
</SearchToolbar>
|
||||||
<PageControls
|
<PageControls
|
||||||
@@ -801,6 +865,7 @@ function JobOutput({ job, eventRelatedSearchableKeys, eventSearchableKeys }) {
|
|||||||
scrollToAlignment="start"
|
scrollToAlignment="start"
|
||||||
width={width || 1}
|
width={width || 1}
|
||||||
overscanRowCount={20}
|
overscanRowCount={20}
|
||||||
|
onScroll={handleScroll}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import logging
|
|||||||
import atexit
|
import atexit
|
||||||
import json
|
import json
|
||||||
import ssl
|
import ssl
|
||||||
|
import datetime
|
||||||
|
|
||||||
from queue import Queue, Empty
|
from queue import Queue, Empty
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
@@ -50,7 +51,7 @@ class WSClient(object):
|
|||||||
|
|
||||||
# Subscription group types
|
# Subscription group types
|
||||||
|
|
||||||
def __init__(self, token=None, hostname='', port=443, secure=True, session_id=None, csrftoken=None):
|
def __init__(self, token=None, hostname='', port=443, secure=True, session_id=None, csrftoken=None, add_received_time=False):
|
||||||
# delay this import, because this is an optional dependency
|
# delay this import, because this is an optional dependency
|
||||||
import websocket
|
import websocket
|
||||||
|
|
||||||
@@ -90,6 +91,7 @@ class WSClient(object):
|
|||||||
self._message_cache = []
|
self._message_cache = []
|
||||||
self._should_subscribe_to_pending_job = False
|
self._should_subscribe_to_pending_job = False
|
||||||
self._pending_unsubscribe = threading.Event()
|
self._pending_unsubscribe = threading.Event()
|
||||||
|
self._add_received_time = add_received_time
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
wst = threading.Thread(target=self._ws_run_forever, args=(self.ws, {"cert_reqs": ssl.CERT_NONE}))
|
wst = threading.Thread(target=self._ws_run_forever, args=(self.ws, {"cert_reqs": ssl.CERT_NONE}))
|
||||||
@@ -195,6 +197,8 @@ class WSClient(object):
|
|||||||
def _on_message(self, message):
|
def _on_message(self, message):
|
||||||
message = json.loads(message)
|
message = json.loads(message)
|
||||||
log.debug('received message: {}'.format(message))
|
log.debug('received message: {}'.format(message))
|
||||||
|
if self._add_received_time:
|
||||||
|
message['received_time'] = datetime.datetime.utcnow()
|
||||||
|
|
||||||
if all([message.get('group_name') == 'jobs', message.get('status') == 'pending', message.get('unified_job_id'), self._should_subscribe_to_pending_job]):
|
if all([message.get('group_name') == 'jobs', message.get('status') == 'pending', message.get('unified_job_id'), self._should_subscribe_to_pending_job]):
|
||||||
if bool(message.get('project_id')) == (self._should_subscribe_to_pending_job['events'] == 'project_update_events'):
|
if bool(message.get('project_id')) == (self._should_subscribe_to_pending_job['events'] == 'project_update_events'):
|
||||||
|
|||||||
Reference in New Issue
Block a user