From 7a5e990ad44c050bcaf8be67ff1a13d6467aeb08 Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Tue, 9 Jan 2024 14:59:29 +0100 Subject: [PATCH] email: rewrite loading of email status using async we start a bunch of requests in the background for each domain. when we switch views immediately, to say the eventlog, these requests are still active in the background. canceling the requests will require a much bigger refactor. https://forum.cloudron.io/topic/10434/email-event-log-loading-very-slowly-seems-tied-to-overall-email-domain-list-health-checks --- CHANGES | 1 + dashboard/src/views/emails.html | 18 +++++-- dashboard/src/views/emails.js | 96 +++++++++++++++++++++++---------- 3 files changed, 84 insertions(+), 31 deletions(-) diff --git a/CHANGES b/CHANGES index e590ba0f6..6a8590856 100644 --- a/CHANGES +++ b/CHANGES @@ -2730,4 +2730,5 @@ * ldap: fix error messages to show proper error messages in the external LDAP connector * dashboard: fix various UI elements hidden for admin user * directoryserver: fix totp validation +* email: improve loading of the mail usage to not block other views from loading diff --git a/dashboard/src/views/emails.html b/dashboard/src/views/emails.html index bd111598d..637528acb 100644 --- a/dashboard/src/views/emails.html +++ b/dashboard/src/views/emails.html @@ -195,10 +195,20 @@ - {{ 'main.loadingPlaceholder' | tr }} ... - {{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount, usage: (domain.usage | prettyDecimalSize) } }} - {{ 'emails.domains.outbound' | tr }} - {{ 'emails.domains.disabled' | tr }} + + {{ 'main.loadingPlaceholder' | tr }} ... + + + + {{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount } }} {{ 'main.loadingPlaceholder' | tr }} ... + {{ 'emails.domains.stats' | tr:{ mailboxCount: domain.mailboxCount, usage: (domain.usage | prettyDecimalSize) } }} + + + {{ 'emails.domains.outbound' | tr }} + {{ 'emails.domains.disabled' | tr }} + + + diff --git a/dashboard/src/views/emails.js b/dashboard/src/views/emails.js index ff507efb5..f9238463c 100644 --- a/dashboard/src/views/emails.js +++ b/dashboard/src/views/emails.js @@ -1,6 +1,7 @@ 'use strict'; /* global $, angular, TASK_TYPES */ +/* global async */ angular.module('Application').controller('EmailsController', ['$scope', '$location', '$translate', '$timeout', 'Client', function ($scope, $location, $translate, $timeout, Client) { Client.onReady(function () { if (!Client.getUserInfo().isAtLeastMailManager) $location.path('/'); }); @@ -404,44 +405,83 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati } }; - function refreshDomainStatuses() { - $scope.domains.forEach(function (domain) { - domain.usage = null; // used by ui to show 'loading' + function refreshMailStatus(domain, done) { + Client.getMailStatusForDomain(domain.domain, function (error, result) { + if (error) { + console.error('Failed to fetch mail status for domain', domain.domain, error); + return done(); + } - Client.getMailStatusForDomain(domain.domain, function (error, result) { - if (error) return console.error('Failed to fetch mail status for domain', domain.domain, error); + domain.status = result; - domain.status = result; + domain.statusOk = Object.keys(result).every(function (k) { + if (k === 'dns') return Object.keys(result.dns).every(function (k) { return result.dns[k].status; }); - domain.statusOk = Object.keys(result).every(function (k) { - if (k === 'dns') return Object.keys(result.dns).every(function (k) { return result.dns[k].status; }); + if (!('status' in result[k])) return true; // if status is not present, the test was not run - if (!('status' in result[k])) return true; // if status is not present, the test was not run - - return result[k].status; - }); + return result[k].status; }); - Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) { - if (error) return console.error('Failed to fetch mail config for domain', domain.domain, error); + done(); + }); + } - domain.inbound = mailConfig.enabled; - domain.outbound = mailConfig.relay.provider !== 'noop'; + function refreshMailConfig(domain, done) { + Client.getMailConfigForDomain(domain.domain, function (error, mailConfig) { + if (error) { + console.error('Failed to fetch mail config for domain', domain.domain, error); + return done(); + } - // do this even if no outbound since people forget to remove mailboxes - Client.getMailboxCount(domain.domain, function (error, count) { - if (error) return console.error('Failed to fetch mailboxes for domain', domain.domain, error); + domain.inbound = mailConfig.enabled; + domain.outbound = mailConfig.relay.provider !== 'noop'; - domain.mailboxCount = count; + // do this even if no outbound since people forget to remove mailboxes + Client.getMailboxCount(domain.domain, function (error, count) { + if (error) { + console.error('Failed to fetch mailboxes for domain', domain.domain, error); + return done(); + } - Client.getMailUsage(domain.domain, function (error, usage) { - if (error) return console.error('Failed to fetch usage for domain', domain.domain, error); + domain.mailboxCount = count; - domain.usage = 0; - // we used to use quotaValue here but it's quite different wrt diskSize. so choose diskSize consistently - // also, quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself - Object.keys(usage).forEach(function (m) { domain.usage += usage[m].diskSize; }); - }); + done(); + }); + }); + } + + function refreshMailUsage(domain, done) { + Client.getMailUsage(domain.domain, function (error, usage) { + if (error) { + console.error('Failed to fetch usage for domain', domain.domain, error); + return done(); + } + + domain.usage = 0; + // we used to use quotaValue here but it's quite different wrt diskSize. so choose diskSize consistently + // also, quotaValue can be missing for deleted mailboxes that are on disk but removed from dovecot/ldap itself + Object.keys(usage).forEach(function (m) { domain.usage += usage[m].diskSize; }); + + done(); + }); + } + + function refreshDomainStatuses() { + async.each($scope.domains, function (domain, iteratorDone) { + async.series([ + refreshMailStatus.bind(null, domain), + refreshMailConfig.bind(null, domain), + ], function () { + domain.loading = false; + iteratorDone(); + }); + }, function () { + // mail usage is loaded separately with a cancellation check. when there are a lot of domains, it runs a long time in background and slows down loading of new views + async.eachLimit($scope.domains, 5, function (domain, itemDone) { + if ($scope.$$destroyed) return itemDone(); // abort! + refreshMailUsage(domain, function () { + domain.loadingUsage = false; + itemDone(); }); }); }); @@ -451,7 +491,9 @@ angular.module('Application').controller('EmailsController', ['$scope', '$locati Client.getDomains(function (error, domains) { if (error) return console.error('Unable to get domain listing.', error); + domains.forEach(function (domain) { domain.loading = true; domain.loadingUsage = true; }); // used by ui to show 'loading' $scope.domains = domains; + $scope.ready = true; if ($scope.user.isAtLeastAdmin) {