Skip to content
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

Add tzdata-legacy to Heroku-24 #340

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Add tzdata-legacy to Heroku-24 #340

wants to merge 1 commit into from

Conversation

joshwlewis
Copy link
Member

@joshwlewis joshwlewis commented Jan 28, 2025

Ubuntu 24.04's tzdata package does not include older/non-standard timezones like US/Eastern and US/Central, where Ubuntu 22.04 does. It's mentioned in the release notes here. Therefore, this is also a change between Heroku-22 and Heroku-24.

Some (mostly Rails) users have these legacy timezone names stored in their application's database, which makes upgrading to Heroku-24 problematic. This PR adds tzdata-legacy to Heroku-24, which restores the removed timezones.

Note that the tzdata-legacy package can't be correctly installed via a buildpack (classic or CNB), since tools like zic and zdump expect the data to be in /usr/share/zoneinfo, which isn't writeable during builds or runtime. This was noted in heroku/heroku-buildpack-apt#140.

I'm not sure if this is totally necessary. Ruby users can alternatively get the legacy timezones via the tzinfo-data RubyGem. The tzinfo-data gem doesn't solve the problem outside of Ruby, but I haven't seen this problem reported outside of a Ruby context.

@joshwlewis joshwlewis requested a review from a team as a code owner January 28, 2025 22:07
@joshwlewis joshwlewis marked this pull request as draft January 28, 2025 22:13
@joshwlewis joshwlewis marked this pull request as ready for review January 28, 2025 22:24
@schneems
Copy link
Contributor

Cross linking (internal only access) tickets:

I'm good with adding this in as it stops the initial pain.

Here's an example error message (to possibly help with people finding the fix to use the tzdata-legacy gem):

~ $ ruby -r"active_support/all" -e "puts Time.now.in_time_zone 'US/Central'"
/app/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.1/lib/active_support/core_ext/time/zones.rb:84:in 'Time.find_zone!': Invalid Timezone: US/Central (ArgumentError)

      ActiveSupport::TimeZone[time_zone] || raise(ArgumentError, "Invalid Timezone: #{time_zone}")
                                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
	from /app/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.1/lib/active_support/core_ext/date_and_time/zones.rb:21:in 'DateAndTime::Zones#in_time_zone'
	from -e:1:in '<main>'

I think the only possible concern I have is considering how we get off this train once we're on it. i.e. what the path to detection, deprecation, and removal might look like in the future. That sounds like a relatively small problem for future-us so I'm in favor of moving forward with adding it here. Saving approval for Ed to have him look over it in the morning.

@schneems
Copy link
Contributor

schneems commented Jan 29, 2025

Looking at the getting started guides:

  • Go: Fails like Ruby
  • Python: Does not fail (requires adding an import)
  • NodeJS: Does not fail Converts automatically for you
  • PHP: Seems to work
  • .NET: Fails
  • JVM
    • Maven: Seems to work
    • Gradle: not tested
    • Scala: not tested

Details

Java Maven

Seems to work:

diff --git a/src/main/java/com/heroku/java/GettingStartedApplication.java b/src/main/java/com/heroku/java/GettingStartedApplication.java
index b4e0c01..d9edef9 100644
--- a/src/main/java/com/heroku/java/GettingStartedApplication.java
+++ b/src/main/java/com/heroku/java/GettingStartedApplication.java
@@ -11,6 +11,10 @@ import java.sql.Connection;
 import java.util.ArrayList;
 import java.util.Map;

+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.ZoneId;
+
 @SpringBootApplication
 @Controller
 public class GettingStartedApplication {
@@ -23,6 +27,10 @@ public class GettingStartedApplication {

     @GetMapping("/")
     public String index() {
+        ZonedDateTime now = ZonedDateTime.now(ZoneId.of("US/Central"));
+        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss z");
+        String formattedTime = now.format(formatter);
+        System.out.println("Current time in US/Central: " + formattedTime);
         return "in
2025-01-29T19:41:44.174000+00:00 app[web.1]: Current time in US/Central: 2025-01-29 13:41:44 CST
2025-01-29T19:41:44.373317+00:00 heroku[router]: at=info method=GET path="/" host=damp-oasis-54967-8e6bcd4f5633.herokuapp.com request_id=dd54d8d1-0884-4ad5-92cd-30454b28fbbf fwd="13.110.54.13" dyno=web.1 connect=0ms service=264ms status=200 bytes=10116 protocol=https
2025-01-29T19:41:44.540934+00:00 heroku[router]: at=info method=GET path="/stylesheets/main.css" host=damp-oasis-54967-8e6bcd4f5633.herokuapp.com request_id=f3eedd95-e7f7-4e5e-9e1e-f26149db46d0 fwd="13.110.54.13" dyno=web.1 connect=0ms service=10ms status=200 bytes=887 protocol=https
2025-01-29T19:41:44.643717+00:00 heroku[router]: at=info method=GET path="/lang-logo.png" host=damp-oasis-54967-8e6bcd4f5633.herokuapp.com request_id=305b998b-f85a-4e3e-90a7-a85794073ce4 fwd="13.110.54.13" dyno=web.1 connect=1ms service=4ms status=200 bytes=4433 protocol=https
2025-01-29T19:41:44.829026+00:00 heroku[router]: at=info method=GET path="/favicon.ico" host=damp-oasis-54967-8e6bcd4f5633.herokuapp.com request_id=0364e2db-7f77-4947-9b28-8b911e2d529d fwd="13.110.54.13" dyno=web.1 connect=0ms service=71ms status=404 bytes=333 protocol=https

.NET

.NET fails on heroku-24:

$ git diff; git add . ; git commit -me ; git push heroku && heroku open && sleep 2 && heroku logs
diff --git a/Frontend/Pages/Index.cshtml.cs b/Frontend/Pages/Index.cshtml.cs
index 0b51174..f8201f7 100644
--- a/Frontend/Pages/Index.cshtml.cs
+++ b/Frontend/Pages/Index.cshtml.cs
@@ -13,7 +13,7 @@ public class IndexModel : PageModel

     public void OnGet()
     {
-        TimeZoneInfo centralZone = TimeZoneInfo.FindSystemTimeZoneById("America/Chicago");
+        TimeZoneInfo centralZone = TimeZoneInfo.FindSystemTimeZoneById("US/Central");
         DateTimeOffset utcTime = DateTimeOffset.UtcNow;
         DateTimeOffset centralTime = TimeZoneInfo.ConvertTime(utcTime, centralZone);
         Console.WriteLine("Current time in US/Central timezone: " + centralTime);

...

2025-01-29T19:30:57.221384+00:00 heroku[web.1]: State changed from starting to up
2025-01-29T19:30:57.932355+00:00 app[web.1]: fail: Microsoft.AspNetCore.Server.Kestrel[13]
2025-01-29T19:30:57.932380+00:00 app[web.1]: Connection id "0HNA0JDQ55SAB", Request id "0HNA0JDQ55SAB:00000001": An unhandled exception was thrown by the application.
2025-01-29T19:30:57.932382+00:00 app[web.1]: System.TimeZoneNotFoundException: The time zone ID 'US/Central' was not found on the local computer.
2025-01-29T19:30:57.932382+00:00 app[web.1]: ---> System.IO.DirectoryNotFoundException: Could not find a part of the path '/usr/share/zoneinfo/US/Central'.
2025-01-29T19:30:57.932382+00:00 app[web.1]: at Interop.ThrowExceptionForIoErrno(ErrorInfo errorInfo, String path, Boolean isDirError)
2025-01-29T19:30:57.932386+00:00 app[web.1]: at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String path, OpenFlags flags, Int32 mode, Boolean failForSymlink, Boolean& wasSymlink, Func`4 createOpenException)
2025-01-29T19:30:57.932387+00:00 app[web.1]: at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, UnixFileMode openPermissions, Int64& fileLength, UnixFileMode& filePermissions, Boolean failForSymlink, Boolean& wasSymlink, Func`4 createOpenException)
2025-01-29T19:30:57.932393+00:00 app[web.1]: at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
2025-01-29T19:30:57.932393+00:00 app[web.1]: at System.TimeZoneInfo.ReadAllBytesFromSeekableNonZeroSizeFile(String path, Int32 maxFileSize)
2025-01-29T19:30:57.932393+00:00 app[web.1]: at System.TimeZoneInfo.TryGetTimeZoneFromLocalMachineCore(String id, TimeZoneInfo& value, Exception& e)
2025-01-29T19:30:57.932394+00:00 app[web.1]: --- End of inner exception stack trace ---
2025-01-29T19:30:57.932398+00:00 app[web.1]: at System.TimeZoneInfo.FindSystemTimeZoneById(String id)
2025-01-29T19:30:57.932398+00:00 app[web.1]: at GettingStarted.Frontend.Pages.IndexModel.OnGet() in /tmp/build_b16f753c/Frontend/Pages/Index.cshtml.cs:line 16
2025-01-29T19:30:57.932399+00:00 app[web.1]: at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.VoidHandlerMethod.Execute(Object receiver, Object[] arguments)
2025-01-29T19:30:57.932399+00:00 app[web.1]: at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
2025-01-29T19:30:57.932399+00:00 app[web.1]: at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
2025-01-29T19:30:57.932399+00:00 app[web.1]: at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
2025-01-29T19:30:57.932410+00:00 app[web.1]: at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
2025-01-29T19:30:57.932410+00:00 app[web.1]: at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
2025-01-29T19:30:57.932411+00:00 app[web.1]: at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)

But works on heroku-22:

2025-01-29T19:33:32.176996+00:00 app[api]: Stack changed from heroku-24 to heroku-22 by user [email protected]
2025-01-29T19:33:45.000000+00:00 app[api]: Build started by user [email protected]
2025-01-29T19:34:34.000000+00:00 app[api]: Build succeeded
2025-01-29T19:34:34.163952+00:00 app[api]: Deploy b1046259 by [email protected]
2025-01-29T19:34:34.163952+00:00 app[api]: Release v5 created by user [email protected]
2025-01-29T19:34:34.398939+00:00 heroku[web.1]: Restarting
2025-01-29T19:34:34.448220+00:00 heroku[web.1]: State changed from up to starting
2025-01-29T19:34:35.176505+00:00 heroku[web.1]: Stopping all processes with SIGTERM
2025-01-29T19:34:35.199726+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2025-01-29T19:34:35.199739+00:00 app[web.1]: Application is shutting down...
2025-01-29T19:34:35.272185+00:00 heroku[web.1]: Process exited with status 0
2025-01-29T19:34:38.467953+00:00 heroku[web.1]: Starting process with command `cd Frontend/bin/publish/; ./Frontend --urls http://*:9194`
2025-01-29T19:34:39.416405+00:00 app[web.1]: warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
2025-01-29T19:34:39.416436+00:00 app[web.1]: No XML encryptor configured. Key {76889b3f-ca64-4320-9d6f-99875b791d66} may be persisted to storage in unencrypted form.
2025-01-29T19:34:39.462591+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[14]
2025-01-29T19:34:39.462593+00:00 app[web.1]: Now listening on: http://[::]:9194
2025-01-29T19:34:39.464344+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2025-01-29T19:34:39.464345+00:00 app[web.1]: Application started. Press Ctrl+C to shut down.
2025-01-29T19:34:39.465128+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2025-01-29T19:34:39.465128+00:00 app[web.1]: Hosting environment: Production
2025-01-29T19:34:39.465129+00:00 app[web.1]: info: Microsoft.Hosting.Lifetime[0]
2025-01-29T19:34:39.465129+00:00 app[web.1]: Content root path: /app/Frontend/bin/publish
2025-01-29T19:34:39.681031+00:00 heroku[web.1]: State changed from starting to up
2025-01-29T19:34:41.393692+00:00 app[web.1]: Current time in US/Central timezone: 01/29/2025 13:34:41 -06:00

PHP

$ git diff; git add . ; git commit -me ; git push heroku

diff --git a/web/index.php b/web/index.php
index cce6795..1ba254b 100755
--- a/web/index.php
+++ b/web/index.php
@@ -35,7 +35,7 @@ $app->addErrorMiddleware(true, false, false);
 // Our web handlers
 $app->get('/', function(Request $request, Response $response, LoggerInterface $logger, Twig $twig) {
   $date = new DateTime('now', new DateTimeZone('US/Central'));
-  $logger->debug($date->format('Y-m-d H:i:s'));
+  $logger->debug($date->format('Y-m-d H:i:s e'));

   $logger->debug('logging output.');
   return $twig->render($response, 'index.twig');
$ heroku logs
2025-01-29T19:16:33.713163+00:00 app[web.1]: [2025-01-29T19:16:33.711508+00:00] default.DEBUG: 2025-01-29 13:16:33 US/Central [] []
2025-01-29T19:16:33.713206+00:00 app[web.1]: [2025-01-29T19:16:33.712949+00:00] default.DEBUG: logging output. [] []

Go

Fails in the same way

$ cat -n main.go
     1	package main
     2
     3	import (
     4		"log"
     5		"net/http"
     6		"os"
     7		"time"
     8
     9		"github.com/gin-gonic/gin"
    10		_ "github.com/heroku/x/hmetrics/onload"
    11	)
    12
    13	func main() {
    14		loc, err := time.LoadLocation("US/Central")
    15		if err != nil {
    16			log.Println("Error:", err)
    17		}

$ heroku logs
2025-01-29T18:41:50.310888+00:00 app[web.1]: 2025/01/29 18:41:50 Error: unknown time zone US/Central

But this same code works fine on heroku-22 with no changes.

Adding _ "time/tzdata" (https://pkg.go.dev/time/tzdata) to the imports fixes the error and produces the correct log output:

2025-01-29T18:57:44.533346+00:00 app[web.1]: 2025/01/29 18:57:44 TimeNow: 2025-01-29 12:57:44.533281371 -0600 CST

Python

From https://docs.python.org/3/library/zoneinfo.html

The zoneinfo module does not directly provide time zone data, and instead pulls time zone information from the system time zone database or the first-party PyPI package tzdata, if available. Some systems, including notably Windows systems, do not have an IANA database available, and so for projects targeting cross-platform compatibility that require time zone data, it is recommended to declare a dependency on tzdata. If neither system data nor tzdata are available, all calls to ZoneInfo will raise ZoneInfoNotFoundError.

Timezones don't work out of the box and the docs say to use the library (which I've not tried, but I am guessing vendors the data rather than relying on the system)

Node.js

Does the conversion for you:

> process.env.TZ = "US/Central"
'US/Central'
> Intl.DateTimeFormat().resolvedOptions().timeZone
'America/Chicago'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants