Skip to content
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
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,16 @@ restarting a `waitCondition()` timeout (#269).
interval therefore uses its fields (a 4-week month), not its real date
span.
- Carbon's default cascade factors make the result independent of the
current date, time zone, PHP version and Carbon major version.
current date and time zone.
- **Correction (#265):** the original notes also claimed independence
from PHP and Carbon major versions. Very large durations containing
millisecond or microsecond parts retain the installed Carbon major's
floating-point behavior. For example,
`'5555555555555 seconds 32037 milliseconds'` converts to
`5555555555588` seconds on Carbon 2 and `5555555555556` on Carbon 3.
SDK/embedded parity on each major is preserved. The regression sample
points do not establish a universal precision bound, and no conversion
behavior changes with this documentation correction.
- **Stricter than the embedded package.** These throw
`InvalidArgumentException` naming the parameter and the accepted forms:
- unknown units, extra words or commas;
Expand Down
21 changes: 18 additions & 3 deletions docs/portal/build/workflows-activities.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,27 @@ measured by their year, month, day, hour, minute, second and microsecond fields
with `CarbonInterval::instance()->totalSeconds`.

Carbon converts with its cascade factors, not the calendar: by default a day is
24 hours, a week 7 days, a month 4 weeks and a year 12 months. The result does
not depend on the current date, time zone, daylight-saving changes, PHP version
or Carbon major version. Changing the factors with
24 hours, a week 7 days, a month 4 weeks and a year 12 months. Those factors do
not depend on the current date, time zone or daylight-saving changes. Changing
the factors with
`CarbonInterval::setCascadeFactors()` changes the result, in the SDK and in the
embedded package, so leave them at their defaults in worker processes.

Conversion retains the installed Carbon major's floating-point behavior. Very
large accepted durations with millisecond or microsecond parts can produce
different whole-second results across Carbon majors. For example,
`'5555555555555 seconds 32037 milliseconds'` converts to `5555555555588`
seconds on Carbon 2 and `5555555555556` on Carbon 3. Each matches the embedded
package on that same Carbon major.

Regression samples span base durations from 10 billion seconds through the
reported value above, with cases around 100 billion and one trillion seconds.
These sample points do not establish a universal precision bound. Even a
one-microsecond part can disappear before
rounding at large magnitudes. Keep the Carbon major consistent when workers
replay runs using such duration strings; explicit integer seconds avoid this
string-conversion difference.

A `CarbonInterval` from `diff()` is measured by its fields too. For example, the
interval from January 1 to February 10, 01:00 is 1 month, 9 days and 1 hour, so
it converts to 37 days and 1 hour. On Carbon 3, the embedded package measures
Expand Down
6 changes: 4 additions & 2 deletions src/Worker/WorkflowDuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
* measured by their y/m/d/h/i/s/f fields with `CarbonInterval::instance()->totalSeconds`, which
* matches the embedded package for every interval that does not carry diff() start and end dates.
* Carbon applies its cascade factors rather than a calendar (by default a day is 24 hours, a week
* 7 days, a month 4 weeks and a year 12 months), so the result does not depend on the current date,
* time zone, PHP version or Carbon major version. Changing the factors globally with
* 7 days, a month 4 weeks and a year 12 months), so calendar dates and time zones do not affect
* measurement. Floating-point precision is retained from the installed Carbon major: very large
* durations containing millisecond or microsecond parts can produce different whole seconds
* across majors. Changing the factors globally with
* `CarbonInterval::setCascadeFactors()` changes the result, as it does in the embedded package.
*
* Numeric strings such as "30" or "1.5" convert as they were coerced when these parameters were
Expand Down
28 changes: 28 additions & 0 deletions tests/WorkflowDurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Carbon\Carbon;
use Carbon\CarbonInterval;
use Composer\InstalledVersions;
use DateInterval;
use DurableWorkflow\Codec\AvroPayloadCodec;
use DurableWorkflow\Exception\NonDeterministicWorkflow;
Expand Down Expand Up @@ -236,6 +237,33 @@ public function testStringsConvertExactlyLikeTheEmbeddedPackage(string $duration
self::assertSame($seconds, WorkflowDuration::seconds($duration, 'timer'));
}

/** @return iterable<string, array{string, array{2: int, 3: int}}> */
public static function largeSubSecondStrings(): iterable
{
yield 'microsecond at ten billion seconds' => ['10000000000 seconds 1 microsecond', [2 => 10_000_000_001, 3 => 10_000_000_001]];
yield 'microsecond lost at one hundred billion seconds' => ['100000000000 seconds 1 microsecond', [2 => 100_000_000_000, 3 => 100_000_000_000]];
yield 'milliseconds at one hundred billion seconds' => ['100000000000 seconds 32037 milliseconds', [2 => 100_000_000_033, 3 => 100_000_000_033]];
yield 'microseconds at one hundred billion seconds' => ['100000000000 seconds 32037000 microseconds', [2 => 100_000_000_033, 3 => 100_000_000_033]];
yield 'milliseconds below one trillion seconds' => ['999999999999 seconds 32037 milliseconds', [2 => 1_000_000_000_032, 3 => 1_000_000_000_032]];
yield 'milliseconds at one trillion seconds' => ['1000000000000 seconds 32037 milliseconds', [2 => 1_000_000_000_033, 3 => 1_000_000_000_001]];
yield 'microseconds at one trillion seconds' => ['1000000000000 seconds 32037000 microseconds', [2 => 1_000_000_000_033, 3 => 1_000_000_000_001]];
yield 'reported milliseconds example' => ['5555555555555 seconds 32037 milliseconds', [2 => 5_555_555_555_588, 3 => 5_555_555_555_556]];
yield 'reported example in microseconds' => ['5555555555555 seconds 32037000 microseconds', [2 => 5_555_555_555_588, 3 => 5_555_555_555_556]];
}

/** @param array{2: int, 3: int} $secondsByCarbonMajor */
#[DataProvider('largeSubSecondStrings')]
public function testLargeSubSecondStringsPreserveTheInstalledCarbonBehavior(string $duration, array $secondsByCarbonMajor): void
{
$major = (int) InstalledVersions::getVersion('nesbot/carbon');
self::assertArrayHasKey($major, $secondsByCarbonMajor);
$seconds = $secondsByCarbonMajor[$major];

self::assertSame($seconds, (int) ceil(CarbonInterval::fromString($duration)->totalSeconds));
self::assertSame($seconds, WorkflowDuration::seconds($duration, 'timer'));
self::assertSame($seconds, self::commandsFor(static fn (WorkflowContext $context) => $context->sleep($duration))[0]['delay_seconds']);
}

public function testIntervalsConvertExactlyLikeTheEmbeddedPackage(): void
{
foreach ([
Expand Down
Loading