From fea8c8e40611581d85ab5f96ea5a11e348c02247 Mon Sep 17 00:00:00 2001
From: Mons Anderson <mons@cpan.org>
Date: Wed, 14 Apr 2021 01:17:44 +0300
Subject: [PATCH] clock: return signed ints from time64 functions

Changed return value type in Lua for the following functions from
`uint64_t` to `int64_t`:

* clock.time64
* clock.realtime64
* clock.monotonic64
* clock.process64
* clock.thread64
* fiber.time64
* fiber.clock64

Changed return value type in Module API for the following functions from
`uint64_t` to `int64_t`:

* fiber_time64
* fiber_clock64

Also enhanced clock.test.lua tap test with more testcases

Due to unsigned values returned from time64 family function time
calculations may lead to undesired value overflows. Having two 64-bit
timestamps I cannot just subtract them without prior comparison. It is
used often, for example when calculating time to a deadline (in queues
or for expiration).

Let's measure time, that pass between two sequential clock.time calls:

```lua
print(clock.time() - clock.time()) -- -9.5367431640625e-07
```

It's ok, with double type time return, but what about 64-bit integers?

```
print(clock.time64() - clock.time64()) -- 18446744073709550616
```

It returns weird integer overflow instead of expected `-1000`

This is counterintuitive and gets conflicted with every time functions
from system headers (which are always signed).

If we consider some hypothetical code, that waits some time for a
deadline, then instead of exiting loop, this code will go to "infinite"
(actually 585 years) wait in the case when the deadline was passed.

```lua
do
local ts = clock.time64() + 1e9 -- some_timestamp_in_future, ex: now+1s
-- ...
local remaining = clock.time64() - ts
-- ...
if remaining > 0 then
    fiber.sleep( tonumber(remaining/1e9) )
end
print "1 second passed"
end
```

This is hard to notice (in real tests deadlines are always in the
future), but in a real environment and under high load such cases occur.

1. Since Lua functions will return ffi.typeof `ctype<int64_t>` instead
   of old `ctype<uint64_t>` any code, that uses _strict_ ffi type check
   will fail. But I hardly believe such code ever exists.
2. Low-level Module API also changed, and code, which compiles with
   `-Werror` may also fail. This could be an issue and in such a case we
   may remove the update of module API change. Still, I consider this
   change reasonable, since all C-level API uses signed time values.

@TarantoolBot document
Title: Return type of time64 functions is changed

These C functions in the public API used to return `uint64_t`, now they
return `int64_t`: `clock_realtime64`, `clock_monotonic64`,
`clock_process64`, `clock_thread64`, `fiber_time64()`,
`fiber_clock64()`.

These Lua functions used to return cdata of type `uint64_t`, now they
return cdata of type `int64_t`: `fiber.time64()`, `fiber.clock64()`.
---
 .../changed-signedness-of-time64-functions.md | 10 +++
 src/lib/core/clock.c                          | 22 +++---
 src/lib/core/clock.h                          | 70 ++++++++++++++++---
 src/lib/core/fiber.c                          |  8 +--
 src/lib/core/fiber.h                          |  4 +-
 src/lua/clock.lua                             |  8 +--
 src/lua/fiber.lua                             |  4 +-
 test/app-tap/clock.test.lua                   | 60 +++++++++++++++-
 8 files changed, 153 insertions(+), 33 deletions(-)
 create mode 100644 changelogs/unreleased/changed-signedness-of-time64-functions.md

diff --git a/changelogs/unreleased/changed-signedness-of-time64-functions.md b/changelogs/unreleased/changed-signedness-of-time64-functions.md
new file mode 100644
index 0000000000..3e500994c0
--- /dev/null
+++ b/changelogs/unreleased/changed-signedness-of-time64-functions.md
@@ -0,0 +1,10 @@
+## bugfix/core
+
+* **[Breaking change]** Return value signedness of 64-bit time functions in
+  `clock` and `fiber` was changed from `uint64_t` to `int64_t` both in Lua
+  and C (gh-5989).
+
+----
+
+Breaking change: lua: return value type for all time64 functions changed from
+`uint64_t` to `int64_t`.
diff --git a/src/lib/core/clock.c b/src/lib/core/clock.c
index ff30584857..ccf35858c8 100644
--- a/src/lib/core/clock.c
+++ b/src/lib/core/clock.c
@@ -70,41 +70,43 @@ clock_thread(void)
 #endif
 }
 
-uint64_t
+int64_t
 clock_realtime64(void)
 {
 	struct timespec ts;
 	clock_gettime(CLOCK_REALTIME, &ts);
-	return ((uint64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
+	return ((int64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
 
 }
-uint64_t
+
+int64_t
 clock_monotonic64(void)
 {
 	struct timespec ts;
 	clock_gettime(CLOCK_MONOTONIC, &ts);
-	return ((uint64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
+	return ((int64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
 }
-uint64_t
+
+int64_t
 clock_process64(void)
 {
 #if defined(CLOCK_PROCESS_CPUTIME_ID)
 	struct timespec ts;
 	clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts);
-	return ((uint64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
+	return ((int64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
 #else
-	return (uint64_t) clock() * 1000000000 / CLOCKS_PER_SEC;
+	return (int64_t)clock() * 1000000000 / CLOCKS_PER_SEC;
 #endif
 }
 
-uint64_t
+int64_t
 clock_thread64(void)
 {
 #if defined(CLOCK_THREAD_CPUTIME_ID)
 	struct timespec ts;
 	clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts);
-	return ((uint64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
+	return ((int64_t)ts.tv_sec) * 1000000000 + ts.tv_nsec;
 #else
-	return (uint64_t) clock() * 1000000000 / CLOCKS_PER_SEC;
+	return (int64_t)clock() * 1000000000 / CLOCKS_PER_SEC;
 #endif
 }
diff --git a/src/lib/core/clock.h b/src/lib/core/clock.h
index e31b8599ad..75376b5614 100644
--- a/src/lib/core/clock.h
+++ b/src/lib/core/clock.h
@@ -40,15 +40,67 @@ extern "C" {
 
 /** \cond public */
 
-double clock_realtime(void);
-double clock_monotonic(void);
-double clock_process(void);
-double clock_thread(void);
-
-uint64_t clock_realtime64(void);
-uint64_t clock_monotonic64(void);
-uint64_t clock_process64(void);
-uint64_t clock_thread64(void);
+/**
+ * A settable system-wide clock that measures real (i.e.,
+ * wall-clock) time.
+ *
+ * See clock_gettime(2), CLOCK_REALTIME.
+ */
+double
+clock_realtime(void);
+
+/**
+ * A nonsettable system-wide clock that represents monotonic time.
+ *
+ * See clock_gettime(2), CLOCK_MONOTONIC.
+ */
+double
+clock_monotonic(void);
+
+/**
+ * A clock that measures CPU time consumed by this process (by all
+ * threads in the process).
+ *
+ * See clock_gettime(2), CLOCK_PROCESS_CPUTIME_ID.
+ */
+double
+clock_process(void);
+
+/**
+ * A clock that measures CPU time consumed by this thread.
+ *
+ * See clock_gettime(2), CLOCK_THREAD_CPUTIME_ID.
+ */
+double
+clock_thread(void);
+
+/**
+ * Same as clock_realtime(), but returns the time as 64 bit
+ * signed integer.
+ */
+int64_t
+clock_realtime64(void);
+
+/**
+ * Same as clock_monotonic(), but returns the time as 64 bit
+ * signed integer.
+ */
+int64_t
+clock_monotonic64(void);
+
+/**
+ * Same as clock_process(), but returns the time as 64 bit
+ * signed integer.
+ */
+int64_t
+clock_process64(void);
+
+/**
+ * Same as clock_thread(), but returns the time as 64 bit
+ * signed integer.
+ */
+int64_t
+clock_thread64(void);
 
 /** \endcond public */
 
diff --git a/src/lib/core/fiber.c b/src/lib/core/fiber.c
index 147ff49b49..19e73d7036 100644
--- a/src/lib/core/fiber.c
+++ b/src/lib/core/fiber.c
@@ -592,10 +592,10 @@ fiber_time(void)
 	return ev_now(loop());
 }
 
-uint64_t
+int64_t
 fiber_time64(void)
 {
-	return (uint64_t) ( ev_now(loop()) * 1000000 + 0.5 );
+	return (int64_t)(ev_now(loop()) * 1000000 + 0.5);
 }
 
 double
@@ -604,10 +604,10 @@ fiber_clock(void)
 	return ev_monotonic_now(loop());
 }
 
-uint64_t
+int64_t
 fiber_clock64(void)
 {
-	return (uint64_t) ( ev_monotonic_now(loop()) * 1000000 + 0.5 );
+	return (int64_t)(ev_monotonic_now(loop()) * 1000000 + 0.5);
 }
 
 /**
diff --git a/src/lib/core/fiber.h b/src/lib/core/fiber.h
index 5d9fd6d6da..4ed5c1d81e 100644
--- a/src/lib/core/fiber.h
+++ b/src/lib/core/fiber.h
@@ -391,7 +391,7 @@ fiber_time(void);
  * Report loop begin time as 64-bit int.
  * Uses real time clock.
  */
-API_EXPORT uint64_t
+API_EXPORT int64_t
 fiber_time64(void);
 
 /**
@@ -405,7 +405,7 @@ fiber_clock(void);
  * Report loop begin time as 64-bit int.
  * Uses monotonic clock.
  */
-API_EXPORT uint64_t
+API_EXPORT int64_t
 fiber_clock64(void);
 
 /**
diff --git a/src/lua/clock.lua b/src/lua/clock.lua
index fee43ccde7..738f3844fe 100644
--- a/src/lua/clock.lua
+++ b/src/lua/clock.lua
@@ -7,10 +7,10 @@ ffi.cdef[[
     double clock_monotonic(void);
     double clock_process(void);
     double clock_thread(void);
-    uint64_t clock_realtime64(void);
-    uint64_t clock_monotonic64(void);
-    uint64_t clock_process64(void);
-    uint64_t clock_thread64(void);
+    int64_t clock_realtime64(void);
+    int64_t clock_monotonic64(void);
+    int64_t clock_process64(void);
+    int64_t clock_thread64(void);
 ]]
 
 local C = ffi.C
diff --git a/src/lua/fiber.lua b/src/lua/fiber.lua
index b141177149..a788aa37d3 100644
--- a/src/lua/fiber.lua
+++ b/src/lua/fiber.lua
@@ -5,11 +5,11 @@ local ffi = require('ffi')
 ffi.cdef[[
 double
 fiber_time(void);
-uint64_t
+int64_t
 fiber_time64(void);
 double
 fiber_clock(void);
-uint64_t
+int64_t
 fiber_clock64(void);
 ]]
 local C = ffi.C
diff --git a/test/app-tap/clock.test.lua b/test/app-tap/clock.test.lua
index d1ea4f0a8e..5f25d77bbc 100755
--- a/test/app-tap/clock.test.lua
+++ b/test/app-tap/clock.test.lua
@@ -1,16 +1,72 @@
 #!/usr/bin/env tarantool
 
 local clock = require("clock")
-local test = require("tap").test("csv")
-test:plan(10)
+local fiber = require("fiber")
+local test = require("tap").test("clock")
+
+test:plan(36)
+
+test:ok(clock.time() > 0, "time")
 test:ok(clock.realtime() > 0, "realtime")
 test:ok(clock.thread() > 0, "thread")
 test:ok(clock.monotonic() > 0, "monotonic")
 test:ok(clock.proc() > 0, "proc")
+test:ok(fiber.time() > 0, "fiber.time")
+test:ok(fiber.clock() > 0, "fiber.clock")
+test:ok(clock.time64() > 0, "time64")
 test:ok(clock.realtime64() > 0, "realtime64")
 test:ok(clock.thread64() > 0, "thread64")
 test:ok(clock.monotonic64() > 0, "monotonic64")
 test:ok(clock.proc64() > 0, "proc64")
+test:ok(fiber.time64() > 0, "fiber.time64")
+test:ok(fiber.clock64() > 0, "fiber.clock64")
 
 test:ok(clock.monotonic() <= clock.monotonic(), "time is monotonic")
+test:ok(clock.monotonic64() <= clock.monotonic64(), "time is monotonic")
 test:ok(math.abs(clock.realtime() - os.time()) < 2, "clock.realtime ~ os.time")
+
+test:ok(fiber.time() == fiber.time(), "fiber.time is cached")
+test:ok(fiber.time64() == fiber.time64(), "fiber.time64 is cached")
+
+test:ok(fiber.clock() == fiber.clock(), "fiber.clock is cached")
+test:ok(fiber.clock64() == fiber.clock64(), "fiber.clock64 is cached")
+test:ok(fiber.clock() < (fiber.yield() or 0) + fiber.clock(),
+        "fiber.clock is growing after yield")
+test:ok(fiber.clock64() < (fiber.yield() or 0) + fiber.clock64(),
+        "fiber.clock64 is growing after yield")
+
+test:ok(math.abs(fiber.time() - tonumber(fiber.time64())/1e6) < 1,
+        "fiber.time64 is in microseconds")
+test:ok(math.abs(fiber.clock() - tonumber(fiber.clock64())/1e6) < 1,
+        "fiber.clock64 is in microseconds")
+
+test:ok(math.abs(clock.time() - tonumber(clock.time64())/1e9) < 1,
+        "clock.time64 is in nanoseconds")
+test:ok(math.abs(clock.realtime() - tonumber(clock.realtime64())/1e9) < 1,
+        "clock.realtime64 is in nanoseconds")
+test:ok(math.abs(clock.thread() - tonumber(clock.thread64())/1e9) < 1,
+        "clock.thread64 is in nanoseconds")
+test:ok(math.abs(clock.proc() - tonumber(clock.proc64())/1e9) < 1,
+        "clock.proc64 is in nanoseconds")
+
+local function subtract_future(func)
+    local ts1 = func()
+    fiber.sleep(0.001)
+    return ts1 - func()
+end
+test:ok(subtract_future(clock.time64) < 0,
+        "clock.time64() can be subtracted")
+test:ok(subtract_future(clock.realtime64) < 0,
+        "clock.realtime64() can be subtracted")
+test:ok(subtract_future(clock.thread64) < 0,
+        "clock.thread64() can be subtracted")
+test:ok(subtract_future(clock.monotonic64) < 0,
+        "clock.monotonic64() can be subtracted")
+test:ok(subtract_future(clock.proc64) < 0,
+        "clock.proc64() can be subtracted")
+test:ok(subtract_future(fiber.time64) < 0,
+        "fiber.time64 can be subtracted")
+test:ok(subtract_future(fiber.clock64) < 0,
+        "fiber.clock64 can be subtracted")
+
+os.exit(test:check() and 0 or 1)
-- 
GitLab