Skip to content

Consider server timezone on _get_timezone_offset instead of django's settings #886

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
May 10, 2025
Merged
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
5 changes: 3 additions & 2 deletions django_celery_beat/schedulers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from django.db.models import Case, F, IntegerField, Q, When
from django.db.models.functions import Cast
from django.db.utils import DatabaseError, InterfaceError
from django.utils import timezone
from kombu.utils.encoding import safe_repr, safe_str
from kombu.utils.json import dumps, loads

Expand Down Expand Up @@ -364,7 +363,9 @@ def _get_timezone_offset(self, timezone_name):
int: The hour offset
"""
# Get server timezone
server_tz = timezone.get_current_timezone()
server_time = aware_now()
# Use server_time.tzinfo directly if it is already a ZoneInfo instance
server_tz = server_time.tzinfo if isinstance(server_time.tzinfo, ZoneInfo) else ZoneInfo(str(server_time.tzinfo))
Copy link

@jennifer-richards jennifer-richards May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tzinfo class does not define __str__. It would be better to use server_time.tzinfo.tzname() which is guaranteed to exist.

There's no guarantee ZoneInfo will know what to do with the tzname(), though, so this won't work with obscure timezone implementations. (Edit: or with obscure zones not known to ZoneInfo. Either of these cases will probably break Django, so I don't think that this is a problem, just pointing it out.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would request to come with a better solution for this in a separate PR, as going to merge this for a hhotfix

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change caused our environment to stop dispatching tasks #894 - this should not have been a patch upgrade

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you state in the docs that changing the timezone needs manual work https://django-celery-beat.readthedocs.io/en/latest/#important-warning-about-time-zones

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alirafiei75 can you also have a look into this please?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locally it is working - on the server, even if i update the server timezone to Europe/Berlin from UTC it doesn't work

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FabianClemenz We wrote some tests to make sure that timezone differences do not affect the tasks running. Can you check them and if possible add a failing test so that I can check what is the expectation? (so then I can fix it)
There was a serious bug in 2.8 and some of the developers that had the problem reviewed the PR #879 and you can see the comments.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alirafiei75 i try to get time for that

For now

  • On server date returns utc timezone
  • Going into docker container, timezone.now() shows utc timezone, timezone.get_current_timezone() shows Europe/Berlin
  • this is also on containers working
  • datetime.now() shows Europe/Berlin

Maybe this line is the error? Getting the current time via datetime instead of timezone?

current_time = datetime.datetime.now()

image

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

localtime of beat shows time in Europe/Berlin - no matter what timezone the server is in

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this line is the error? Getting the current time via datetime instead of timezone?

This could not be the problem as it is not used anywhere else and just the timedelta matters and as long as both current_time and _last_full_sync are calculated in the same way, there won't be a problem.


if isinstance(timezone_name, ZoneInfo):
timezone_name = timezone_name.key
Expand Down
52 changes: 52 additions & 0 deletions t/unit/test_schedulers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1453,3 +1453,55 @@ def mock_apply_async(*args, **kwargs):
ma.run_tasks(self.request, PeriodicTask.objects.filter(id=self.m1.id))
assert 'periodic_task_name' in self.captured_headers
assert self.captured_headers['periodic_task_name'] == self.m1.name



@pytest.mark.django_db
class test_timezone_offset_handling:
def setup_method(self):
self.app = patch("django_celery_beat.schedulers.current_app").start()

def teardown_method(self):
patch.stopall()

@patch("django_celery_beat.schedulers.aware_now")
def test_server_timezone_handling_with_zoneinfo(self, mock_aware_now):
"""Test handling when server timezone is already a ZoneInfo instance."""

# Create a mock scheduler with only the methods we need to test
class MockScheduler:
_get_timezone_offset = schedulers.DatabaseScheduler._get_timezone_offset

s = MockScheduler()

tokyo_tz = ZoneInfo("Asia/Tokyo")
mock_now = datetime(2023, 1, 1, 12, 0, 0, tzinfo=tokyo_tz)
mock_aware_now.return_value = mock_now

# Test with a different timezone
new_york_tz = "America/New_York"
offset = s._get_timezone_offset(new_york_tz) # Pass self explicitly

# Tokyo is UTC+9, New York is UTC-5, so difference should be 14 hours
assert offset == 14
assert mock_aware_now.called

@patch("django_celery_beat.schedulers.aware_now")
def test_timezone_offset_with_zoneinfo_object_param(self, mock_aware_now):
"""Test handling when timezone_name parameter is a ZoneInfo object."""

class MockScheduler:
_get_timezone_offset = schedulers.DatabaseScheduler._get_timezone_offset

s = MockScheduler()

tokyo_tz = ZoneInfo("Asia/Tokyo")
mock_now = datetime(2023, 1, 1, 12, 0, 0, tzinfo=tokyo_tz)
mock_aware_now.return_value = mock_now

# Test with a ZoneInfo object as parameter
new_york_tz = ZoneInfo("America/New_York")
offset = s._get_timezone_offset(new_york_tz) # Pass self explicitly

# Tokyo is UTC+9, New York is UTC-5, so difference should be 14 hours
assert offset == 14