MM-61947 Run DND expiry job more often and round expiry time to match interval (#29938)
* MM-61947 Run DND expiry job more often and round expiry time to match interval * Move comment to make it godoc-compatible * Change truncateDNDEndTime to work with seconds --------- Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
e8ef26196c
Коммит
3902d00d0f
@@ -7,6 +7,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
||||||
@@ -417,7 +418,7 @@ func (ps *PlatformService) SetStatusDoNotDisturbTimed(userID string, endtime int
|
|||||||
status.Status = model.StatusDnd
|
status.Status = model.StatusDnd
|
||||||
status.Manual = true
|
status.Manual = true
|
||||||
|
|
||||||
status.DNDEndTime = endtime
|
status.DNDEndTime = truncateDNDEndTime(endtime)
|
||||||
|
|
||||||
ps.SaveAndBroadcastStatus(status)
|
ps.SaveAndBroadcastStatus(status)
|
||||||
if ps.sharedChannelService != nil {
|
if ps.sharedChannelService != nil {
|
||||||
@@ -425,6 +426,19 @@ func (ps *PlatformService) SetStatusDoNotDisturbTimed(userID string, endtime int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// truncateDNDEndTime takes a user-provided timestamp (in seconds) for when their DND expiry should end and truncates
|
||||||
|
// it to line up with the DND expiry job so that the user's DND time doesn't expire late by an interval. The job to
|
||||||
|
// expire statuses runs every minute currently, so this trims the seconds and milliseconds off the given timestamp.
|
||||||
|
//
|
||||||
|
// This will result in statuses expiring slightly earlier than specified in the UI, but the status will expire at
|
||||||
|
// the correct time on the wall clock. For example, if the time is currently 13:04:29 and the user sets the expiry to
|
||||||
|
// 5 minutes, truncating will make the status will expire at 13:09:00 instead of at 13:10:00.
|
||||||
|
//
|
||||||
|
// Note that the timestamps used by this are in seconds, not milliseconds. This matches UserStatus.DNDEndTime.
|
||||||
|
func truncateDNDEndTime(endtime int64) int64 {
|
||||||
|
return time.Unix(endtime, 0).Truncate(model.DNDExpiryInterval).Unix()
|
||||||
|
}
|
||||||
|
|
||||||
func (ps *PlatformService) SetStatusDoNotDisturb(userID string) {
|
func (ps *PlatformService) SetStatusDoNotDisturb(userID string) {
|
||||||
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
if !*ps.Config().ServiceSettings.EnableUserStatuses {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ package platform
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/mattermost/mattermost/server/public/model"
|
"github.com/mattermost/mattermost/server/public/model"
|
||||||
@@ -37,3 +38,17 @@ func TestSaveStatus(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTruncateDNDEndTime(t *testing.T) {
|
||||||
|
// 2025-Jan-20 at 17:13:32 GMT becomes 17:13:00
|
||||||
|
assert.Equal(t, int64(1737393180), truncateDNDEndTime(1737393212))
|
||||||
|
|
||||||
|
// 2025-Jan-20 at 17:13:00 GMT remains unchanged
|
||||||
|
assert.Equal(t, int64(1737393180), truncateDNDEndTime(1737393180))
|
||||||
|
|
||||||
|
// 2025-Jan-20 at 00:00:10 GMT becomes 00:00:00
|
||||||
|
assert.Equal(t, int64(1737331200), truncateDNDEndTime(1737331210))
|
||||||
|
|
||||||
|
// 2025-Jan-20 at 00:00:10 GMT remains unchanged
|
||||||
|
assert.Equal(t, int64(1737331200), truncateDNDEndTime(1737331200))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1782,14 +1782,14 @@ func cancelTask(mut *sync.Mutex, taskPointer **model.ScheduledTask) {
|
|||||||
func runDNDStatusExpireJob(a *App) {
|
func runDNDStatusExpireJob(a *App) {
|
||||||
if a.IsLeader() {
|
if a.IsLeader() {
|
||||||
withMut(&a.ch.dndTaskMut, func() {
|
withMut(&a.ch.dndTaskMut, func() {
|
||||||
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute)
|
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, model.DNDExpiryInterval)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
a.ch.srv.AddClusterLeaderChangedListener(func() {
|
a.ch.srv.AddClusterLeaderChangedListener(func() {
|
||||||
mlog.Info("Cluster leader changed. Determining if unset DNS status task should be running", mlog.Bool("isLeader", a.IsLeader()))
|
mlog.Info("Cluster leader changed. Determining if unset DNS status task should be running", mlog.Bool("isLeader", a.IsLeader()))
|
||||||
if a.IsLeader() {
|
if a.IsLeader() {
|
||||||
withMut(&a.ch.dndTaskMut, func() {
|
withMut(&a.ch.dndTaskMut, func() {
|
||||||
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, 5*time.Minute)
|
a.ch.dndTask = model.CreateRecurringTaskFromNextIntervalTime("Unset DND Statuses", a.UpdateDNDStatusOfUsers, model.DNDExpiryInterval)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
cancelTask(&a.ch.dndTaskMut, &a.ch.dndTask)
|
cancelTask(&a.ch.dndTaskMut, &a.ch.dndTask)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -16,6 +17,9 @@ const (
|
|||||||
StatusCacheSize = SessionCacheSize
|
StatusCacheSize = SessionCacheSize
|
||||||
StatusChannelTimeout = 20000 // 20 seconds
|
StatusChannelTimeout = 20000 // 20 seconds
|
||||||
StatusMinUpdateTime = 120000 // 2 minutes
|
StatusMinUpdateTime = 120000 // 2 minutes
|
||||||
|
|
||||||
|
// DNDExpiryInterval is how often the job to expire temporary DND statuses runs.
|
||||||
|
DNDExpiryInterval = 1 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
type Status struct {
|
type Status struct {
|
||||||
@@ -24,7 +28,11 @@ type Status struct {
|
|||||||
Manual bool `json:"manual"`
|
Manual bool `json:"manual"`
|
||||||
LastActivityAt int64 `json:"last_activity_at"`
|
LastActivityAt int64 `json:"last_activity_at"`
|
||||||
ActiveChannel string `json:"active_channel,omitempty" db:"-"`
|
ActiveChannel string `json:"active_channel,omitempty" db:"-"`
|
||||||
|
|
||||||
|
// DNDEndTime is the time that the user's DND status will expire. Unlike other timestamps in Mattermost, this value
|
||||||
|
// is in seconds instead of milliseconds.
|
||||||
DNDEndTime int64 `json:"dnd_end_time"`
|
DNDEndTime int64 `json:"dnd_end_time"`
|
||||||
|
|
||||||
PrevStatus string `json:"-"`
|
PrevStatus string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
|||||||
|
|
||||||
import Constants, {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
import Constants, {A11yCustomEventTypes, UserStatuses} from 'utils/constants';
|
||||||
import type {A11yFocusEventDetail} from 'utils/constants';
|
import type {A11yFocusEventDetail} from 'utils/constants';
|
||||||
import {toUTCUnix} from 'utils/datetime';
|
import {toUTCUnixInSeconds} from 'utils/datetime';
|
||||||
import {isKeyPressed} from 'utils/keyboard';
|
import {isKeyPressed} from 'utils/keyboard';
|
||||||
import {localizeMessage} from 'utils/utils';
|
import {localizeMessage} from 'utils/utils';
|
||||||
|
|
||||||
@@ -126,9 +126,9 @@ export default class DndCustomTimePicker extends React.PureComponent<Props, Stat
|
|||||||
await this.props.actions.setStatus({
|
await this.props.actions.setStatus({
|
||||||
user_id: this.props.userId,
|
user_id: this.props.userId,
|
||||||
status: UserStatuses.DND,
|
status: UserStatuses.DND,
|
||||||
dnd_end_time: toUTCUnix(endTime),
|
dnd_end_time: toUTCUnixInSeconds(endTime),
|
||||||
manual: true,
|
manual: true,
|
||||||
last_activity_at: toUTCUnix(this.props.currentDate),
|
last_activity_at: toUTCUnixInSeconds(this.props.currentDate),
|
||||||
});
|
});
|
||||||
this.props.onExited();
|
this.props.onExited();
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import * as Menu from 'components/menu';
|
|||||||
import PostReminderCustomTimePicker from 'components/post_reminder_custom_time_picker_modal';
|
import PostReminderCustomTimePicker from 'components/post_reminder_custom_time_picker_modal';
|
||||||
|
|
||||||
import {ModalIdentifiers} from 'utils/constants';
|
import {ModalIdentifiers} from 'utils/constants';
|
||||||
import {toUTCUnix} from 'utils/datetime';
|
import {toUTCUnixInSeconds} from 'utils/datetime';
|
||||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -67,7 +67,7 @@ function PostReminderSubmenu(props: Props) {
|
|||||||
endTime = currentDate.add(1, 'day').set({hour: 9, minute: 0});
|
endTime = currentDate.add(1, 'day').set({hour: 9, minute: 0});
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(addPostReminder(props.userId, props.post.id, toUTCUnix(endTime.toDate())));
|
dispatch(addPostReminder(props.userId, props.post.id, toUTCUnixInSeconds(endTime.toDate())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {useIntl} from 'react-intl';
|
|||||||
import {getRoundedTime} from 'components/custom_status/date_time_input';
|
import {getRoundedTime} from 'components/custom_status/date_time_input';
|
||||||
import DateTimePickerModal from 'components/date_time_picker_modal/date_time_picker_modal';
|
import DateTimePickerModal from 'components/date_time_picker_modal/date_time_picker_modal';
|
||||||
|
|
||||||
import {toUTCUnix} from 'utils/datetime';
|
import {toUTCUnixInSeconds} from 'utils/datetime';
|
||||||
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
import {getCurrentMomentForTimezone} from 'utils/timezone';
|
||||||
|
|
||||||
import type {PropsFromRedux} from './index';
|
import type {PropsFromRedux} from './index';
|
||||||
@@ -31,7 +31,7 @@ function PostReminderCustomTimePicker({userId, timezone, onExited, postId, actio
|
|||||||
const initialReminderTime = getRoundedTime(currentTime);
|
const initialReminderTime = getRoundedTime(currentTime);
|
||||||
|
|
||||||
const handleConfirm = useCallback((dateTime: Moment) => {
|
const handleConfirm = useCallback((dateTime: Moment) => {
|
||||||
actions.addPostReminder(userId, postId, toUTCUnix(dateTime.toDate()));
|
actions.addPostReminder(userId, postId, toUTCUnixInSeconds(dateTime.toDate()));
|
||||||
onExited();
|
onExited();
|
||||||
}, [actions, postId, userId, onExited]);
|
}, [actions, postId, userId, onExited]);
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function isYesterday(date: Date): boolean {
|
|||||||
return isSameDay(date, yesterday);
|
return isSameDay(date, yesterday);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toUTCUnix(date: Date): number {
|
export function toUTCUnixInSeconds(date: Date): number {
|
||||||
return Math.round(new Date(date.toISOString()).getTime() / 1000);
|
return Math.round(new Date(date.toISOString()).getTime() / 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -100,6 +100,11 @@ export type UserStatus = {
|
|||||||
manual?: boolean;
|
manual?: boolean;
|
||||||
last_activity_at?: number;
|
last_activity_at?: number;
|
||||||
active_channel?: string;
|
active_channel?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time when a user's timed DND status will expire. Unlike other timestamps in the app, this is in seconds
|
||||||
|
* instead of milliseconds.
|
||||||
|
*/
|
||||||
dnd_end_time?: number;
|
dnd_end_time?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user