From 3902d00d0f08b2362a8716b311867ae2585b7243 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 25 Feb 2025 16:20:00 -0500 Subject: [PATCH] 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 --- server/channels/app/platform/status.go | 16 +++++++++++++++- server/channels/app/platform/status_test.go | 15 +++++++++++++++ server/channels/app/server.go | 4 ++-- server/public/model/status.go | 12 ++++++++++-- .../dnd_custom_time_picker_modal.tsx | 6 +++--- .../dot_menu/post_reminder_submenu.tsx | 4 ++-- .../post_reminder_custom_time_picker_modal.tsx | 4 ++-- webapp/channels/src/utils/datetime.ts | 2 +- webapp/platform/types/src/users.ts | 5 +++++ 9 files changed, 55 insertions(+), 13 deletions(-) diff --git a/server/channels/app/platform/status.go b/server/channels/app/platform/status.go index 5aee2f3abb..d8bf6609e3 100644 --- a/server/channels/app/platform/status.go +++ b/server/channels/app/platform/status.go @@ -7,6 +7,7 @@ import ( "encoding/json" "errors" "net/http" + "time" "github.com/mattermost/mattermost/server/public/model" "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.Manual = true - status.DNDEndTime = endtime + status.DNDEndTime = truncateDNDEndTime(endtime) ps.SaveAndBroadcastStatus(status) 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) { if !*ps.Config().ServiceSettings.EnableUserStatuses { return diff --git a/server/channels/app/platform/status_test.go b/server/channels/app/platform/status_test.go index bbb4d6d57a..7f791d555a 100644 --- a/server/channels/app/platform/status_test.go +++ b/server/channels/app/platform/status_test.go @@ -6,6 +6,7 @@ package platform import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "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)) +} diff --git a/server/channels/app/server.go b/server/channels/app/server.go index 5a8b815279..6518488072 100644 --- a/server/channels/app/server.go +++ b/server/channels/app/server.go @@ -1782,14 +1782,14 @@ func cancelTask(mut *sync.Mutex, taskPointer **model.ScheduledTask) { func runDNDStatusExpireJob(a *App) { if a.IsLeader() { 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() { mlog.Info("Cluster leader changed. Determining if unset DNS status task should be running", mlog.Bool("isLeader", a.IsLeader())) if a.IsLeader() { 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 { cancelTask(&a.ch.dndTaskMut, &a.ch.dndTask) diff --git a/server/public/model/status.go b/server/public/model/status.go index a0c5d073f3..6c400063fe 100644 --- a/server/public/model/status.go +++ b/server/public/model/status.go @@ -5,6 +5,7 @@ package model import ( "encoding/json" + "time" ) const ( @@ -16,6 +17,9 @@ const ( StatusCacheSize = SessionCacheSize StatusChannelTimeout = 20000 // 20 seconds StatusMinUpdateTime = 120000 // 2 minutes + + // DNDExpiryInterval is how often the job to expire temporary DND statuses runs. + DNDExpiryInterval = 1 * time.Minute ) type Status struct { @@ -24,8 +28,12 @@ type Status struct { Manual bool `json:"manual"` LastActivityAt int64 `json:"last_activity_at"` ActiveChannel string `json:"active_channel,omitempty" db:"-"` - DNDEndTime int64 `json:"dnd_end_time"` - PrevStatus string `json:"-"` + + // 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"` + + PrevStatus string `json:"-"` } func (s *Status) ToJSON() ([]byte, error) { diff --git a/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx b/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx index d68d576c9e..92f9e06db3 100644 --- a/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx +++ b/webapp/channels/src/components/dnd_custom_time_picker_modal/dnd_custom_time_picker_modal.tsx @@ -21,7 +21,7 @@ import MenuWrapper from 'components/widgets/menu/menu_wrapper'; import Constants, {A11yCustomEventTypes, UserStatuses} 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 {localizeMessage} from 'utils/utils'; @@ -126,9 +126,9 @@ export default class DndCustomTimePicker extends React.PureComponent { - actions.addPostReminder(userId, postId, toUTCUnix(dateTime.toDate())); + actions.addPostReminder(userId, postId, toUTCUnixInSeconds(dateTime.toDate())); onExited(); }, [actions, postId, userId, onExited]); diff --git a/webapp/channels/src/utils/datetime.ts b/webapp/channels/src/utils/datetime.ts index a359f392fb..7c957f5a51 100644 --- a/webapp/channels/src/utils/datetime.ts +++ b/webapp/channels/src/utils/datetime.ts @@ -86,7 +86,7 @@ export function isYesterday(date: Date): boolean { 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); } diff --git a/webapp/platform/types/src/users.ts b/webapp/platform/types/src/users.ts index 8ce683bdfb..16bdf590e3 100644 --- a/webapp/platform/types/src/users.ts +++ b/webapp/platform/types/src/users.ts @@ -100,6 +100,11 @@ export type UserStatus = { manual?: boolean; last_activity_at?: number; 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; };