authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-15 11:24:26+00:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-12-21 13:07:04+00:00
log67eed9955005aaa02344b8d566ba140b9f2e0e18
tree1f11358b2a1fd708738a93df52ff18175a7e2c7c
parentfa7e818e144f2fd316fc54492f4699f6e1524738
signaturelock-open Commit is signed but in an unrecognized format.

std.Io.Queue: introduce closure and fix a bug

Queues can now be "closed". A closed queue cannot have more elements appended with `put`, and blocked calls to `put` will immediately unblock having failed to append some elements. Calls to `get` will continue to succeed as long as the queue buffer is non-empty, but will then never block; already-blocked calls to `get` will unblock. All queue get/put operations can now return `error.Closed` to indicate that the queue has been closed. For bulk get/put operations, they may add/receive fewer elements than the minimum requested *if* the queue was closed or the calling task was canceled. In that case, if any elements were already added/received, they are returned first, and successive calls will return `error.Closed` or `error.Canceled`. Also, fix a bug where `Queue.get` could deadlock because it incorrectly blocked until the given buffer was *filled*. Resolves: #30141

5 files changed, 370 insertions(+), 164 deletions(-)

lib/std/Io.zig+221-82
......@@ -701,7 +701,7 @@ pub const VTable = struct {
701701 netClose: *const fn (?*anyopaque, handle: net.Socket.Handle) void,
702702 netInterfaceNameResolve: *const fn (?*anyopaque, *const net.Interface.Name) net.Interface.Name.ResolveError!net.Interface,
703703 netInterfaceName: *const fn (?*anyopaque, net.Interface) net.Interface.NameError!net.Interface.Name,
704 netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) void,
704 netLookup: *const fn (?*anyopaque, net.HostName, *Queue(net.HostName.LookupResult), net.HostName.LookupOptions) net.HostName.LookupError!void,
705705};
706706
707707pub const Cancelable = error{
......@@ -1208,7 +1208,9 @@ pub fn Select(comptime U: type) type {
12081208 const args_casted: *const Args = @ptrCast(@alignCast(context));
12091209 const unerased_select: *S = @fieldParentPtr("group", group);
12101210 const elem = @unionInit(U, @tagName(field), @call(.auto, function, args_casted.*));
1211 unerased_select.queue.putOneUncancelable(unerased_select.io, elem);
1211 unerased_select.queue.putOneUncancelable(unerased_select.io, elem) catch |err| switch (err) {
1212 error.Closed => unreachable,
1213 };
12121214 }
12131215 };
12141216 _ = @atomicRmw(usize, &s.outstanding, .Add, 1, .monotonic);
......@@ -1222,7 +1224,10 @@ pub fn Select(comptime U: type) type {
12221224 /// Not threadsafe.
12231225 pub fn wait(s: *S) Cancelable!U {
12241226 s.outstanding -= 1;
1225 return s.queue.getOne(s.io);
1227 return s.queue.getOne(s.io) catch |err| switch (err) {
1228 error.Canceled => |e| return e,
1229 error.Closed => unreachable,
1230 };
12261231 }
12271232
12281233 /// Equivalent to `wait` but requests cancellation on all remaining
......@@ -1569,8 +1574,11 @@ pub const Event = enum(u32) {
15691574 }
15701575};
15711576
1577pub const QueueClosedError = error{Closed};
1578
15721579pub const TypeErasedQueue = struct {
15731580 mutex: Mutex,
1581 closed: bool,
15741582
15751583 /// Ring buffer. This data is logically *after* queued getters.
15761584 buffer: []u8,
......@@ -1582,12 +1590,14 @@ pub const TypeErasedQueue = struct {
15821590
15831591 const Put = struct {
15841592 remaining: []const u8,
1593 needed: usize,
15851594 condition: Condition,
15861595 node: std.DoublyLinkedList.Node,
15871596 };
15881597
15891598 const Get = struct {
15901599 remaining: []u8,
1600 needed: usize,
15911601 condition: Condition,
15921602 node: std.DoublyLinkedList.Node,
15931603 };
......@@ -1595,6 +1605,7 @@ pub const TypeErasedQueue = struct {
15951605 pub fn init(buffer: []u8) TypeErasedQueue {
15961606 return .{
15971607 .mutex = .init,
1608 .closed = false,
15981609 .buffer = buffer,
15991610 .start = 0,
16001611 .len = 0,
......@@ -1603,7 +1614,27 @@ pub const TypeErasedQueue = struct {
16031614 };
16041615 }
16051616
1606 pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) Cancelable!usize {
1617 pub fn close(q: *TypeErasedQueue, io: Io) void {
1618 q.mutex.lockUncancelable(io);
1619 defer q.mutex.unlock(io);
1620 q.closed = true;
1621 {
1622 var it = q.getters.first;
1623 while (it) |node| : (it = node.next) {
1624 const getter: *Get = @alignCast(@fieldParentPtr("node", node));
1625 getter.condition.signal(io);
1626 }
1627 }
1628 {
1629 var it = q.putters.first;
1630 while (it) |node| : (it = node.next) {
1631 const putter: *Put = @alignCast(@fieldParentPtr("node", node));
1632 putter.condition.signal(io);
1633 }
1634 }
1635 }
1636
1637 pub fn put(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) (QueueClosedError || Cancelable)!usize {
16071638 assert(elements.len >= min);
16081639 if (elements.len == 0) return 0;
16091640 try q.mutex.lock(io);
......@@ -1614,13 +1645,14 @@ pub const TypeErasedQueue = struct {
16141645 /// Same as `put`, except does not introduce a cancelation point.
16151646 ///
16161647 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1617 pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) usize {
1648 pub fn putUncancelable(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize) QueueClosedError!usize {
16181649 assert(elements.len >= min);
16191650 if (elements.len == 0) return 0;
16201651 q.mutex.lockUncancelable(io);
16211652 defer q.mutex.unlock(io);
16221653 return q.putLocked(io, elements, min, true) catch |err| switch (err) {
16231654 error.Canceled => unreachable,
1655 error.Closed => |e| return e,
16241656 };
16251657 }
16261658
......@@ -1634,49 +1666,79 @@ pub const TypeErasedQueue = struct {
16341666 return if (slice.len > 0) slice else null;
16351667 }
16361668
1637 fn putLocked(q: *TypeErasedQueue, io: Io, elements: []const u8, min: usize, uncancelable: bool) Cancelable!usize {
1669 fn putLocked(q: *TypeErasedQueue, io: Io, elements: []const u8, target: usize, uncancelable: bool) (QueueClosedError || Cancelable)!usize {
1670 // A closed queue cannot be added to, even if there is space in the buffer.
1671 if (q.closed) return error.Closed;
1672
16381673 // Getters have first priority on the data, and only when the getters
16391674 // queue is empty do we start populating the buffer.
16401675
1641 var remaining = elements;
1676 // The number of elements we add immediately, before possibly blocking.
1677 var n: usize = 0;
1678
16421679 while (q.getters.popFirst()) |getter_node| {
16431680 const getter: *Get = @alignCast(@fieldParentPtr("node", getter_node));
1644 const copy_len = @min(getter.remaining.len, remaining.len);
1681 const copy_len = @min(getter.remaining.len, elements.len - n);
16451682 assert(copy_len > 0);
1646 @memcpy(getter.remaining[0..copy_len], remaining[0..copy_len]);
1647 remaining = remaining[copy_len..];
1683 @memcpy(getter.remaining[0..copy_len], elements[n..][0..copy_len]);
16481684 getter.remaining = getter.remaining[copy_len..];
1649 if (getter.remaining.len == 0) {
1685 getter.needed -|= copy_len;
1686 n += copy_len;
1687 if (getter.needed == 0) {
16501688 getter.condition.signal(io);
1651 if (remaining.len > 0) continue;
1652 } else q.getters.prepend(getter_node);
1653 assert(remaining.len == 0);
1654 return elements.len;
1689 } else {
1690 assert(n == elements.len); // we didn't have enough elements for the getter
1691 q.getters.prepend(getter_node);
1692 }
1693 if (n == elements.len) return elements.len;
16551694 }
16561695
16571696 while (q.puttableSlice()) |slice| {
1658 const copy_len = @min(slice.len, remaining.len);
1697 const copy_len = @min(slice.len, elements.len - n);
16591698 assert(copy_len > 0);
1660 @memcpy(slice[0..copy_len], remaining[0..copy_len]);
1699 @memcpy(slice[0..copy_len], elements[n..][0..copy_len]);
16611700 q.len += copy_len;
1662 remaining = remaining[copy_len..];
1663 if (remaining.len == 0) return elements.len;
1701 n += copy_len;
1702 if (n == elements.len) return elements.len;
16641703 }
16651704
1666 const total_filled = elements.len - remaining.len;
1667 if (total_filled >= min) return total_filled;
1705 // Don't block if we hit the target.
1706 if (n >= target) return n;
16681707
1669 var pending: Put = .{ .remaining = remaining, .condition = .{}, .node = .{} };
1708 var pending: Put = .{
1709 .remaining = elements[n..],
1710 .needed = target - n,
1711 .condition = .init,
1712 .node = .{},
1713 };
16701714 q.putters.append(&pending.node);
1671 defer if (pending.remaining.len > 0) q.putters.remove(&pending.node);
1672 while (pending.remaining.len > 0) if (uncancelable)
1673 pending.condition.waitUncancelable(io, &q.mutex)
1674 else
1675 try pending.condition.wait(io, &q.mutex);
1676 return elements.len;
1715 defer if (pending.needed > 0) q.putters.remove(&pending.node);
1716
1717 while (pending.needed > 0 and !q.closed) {
1718 if (uncancelable) {
1719 pending.condition.waitUncancelable(io, &q.mutex);
1720 continue;
1721 }
1722 pending.condition.wait(io, &q.mutex) catch |err| switch (err) {
1723 error.Canceled => if (pending.remaining.len == elements.len) {
1724 // Canceled while waiting, and appended no elements.
1725 return error.Canceled;
1726 } else {
1727 // Canceled while waiting, but appended some elements, so report those first.
1728 io.recancel();
1729 return elements.len - pending.remaining.len;
1730 },
1731 };
1732 }
1733 if (pending.remaining.len == elements.len) {
1734 // The queue was closed while we were waiting. We appended no elements.
1735 assert(q.closed);
1736 return error.Closed;
1737 }
1738 return elements.len - pending.remaining.len;
16771739 }
16781740
1679 pub fn get(q: *@This(), io: Io, buffer: []u8, min: usize) Cancelable!usize {
1741 pub fn get(q: *TypeErasedQueue, io: Io, buffer: []u8, min: usize) (QueueClosedError || Cancelable)!usize {
16801742 assert(buffer.len >= min);
16811743 if (buffer.len == 0) return 0;
16821744 try q.mutex.lock(io);
......@@ -1687,13 +1749,14 @@ pub const TypeErasedQueue = struct {
16871749 /// Same as `get`, except does not introduce a cancelation point.
16881750 ///
16891751 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1690 pub fn getUncancelable(q: *TypeErasedQueue, io: Io, buffer: []u8, min: usize) usize {
1752 pub fn getUncancelable(q: *TypeErasedQueue, io: Io, buffer: []u8, min: usize) QueueClosedError!usize {
16911753 assert(buffer.len >= min);
16921754 if (buffer.len == 0) return 0;
16931755 q.mutex.lockUncancelable(io);
16941756 defer q.mutex.unlock(io);
16951757 return q.getLocked(io, buffer, min, true) catch |err| switch (err) {
16961758 error.Canceled => unreachable,
1759 error.Closed => |e| return e,
16971760 };
16981761 }
16991762
......@@ -1703,21 +1766,23 @@ pub const TypeErasedQueue = struct {
17031766 return if (slice.len > 0) slice else null;
17041767 }
17051768
1706 fn getLocked(q: *@This(), io: Io, buffer: []u8, min: usize, uncancelable: bool) Cancelable!usize {
1769 fn getLocked(q: *TypeErasedQueue, io: Io, buffer: []u8, target: usize, uncancelable: bool) (QueueClosedError || Cancelable)!usize {
17071770 // The ring buffer gets first priority, then data should come from any
17081771 // queued putters, then finally the ring buffer should be filled with
17091772 // data from putters so they can be resumed.
17101773
1711 var remaining = buffer;
1774 // The number of elements we received immediately, before possibly blocking.
1775 var n: usize = 0;
1776
17121777 while (q.gettableSlice()) |slice| {
1713 const copy_len = @min(slice.len, remaining.len);
1778 const copy_len = @min(slice.len, buffer.len - n);
17141779 assert(copy_len > 0);
1715 @memcpy(remaining[0..copy_len], slice[0..copy_len]);
1780 @memcpy(buffer[n..][0..copy_len], slice[0..copy_len]);
17161781 q.start += copy_len;
17171782 if (q.buffer.len - q.start == 0) q.start = 0;
17181783 q.len -= copy_len;
1719 remaining = remaining[copy_len..];
1720 if (remaining.len == 0) {
1784 n += copy_len;
1785 if (n == buffer.len) {
17211786 q.fillRingBufferFromPutters(io);
17221787 return buffer.len;
17231788 }
......@@ -1726,33 +1791,64 @@ pub const TypeErasedQueue = struct {
17261791 // Copy directly from putters into buffer.
17271792 while (q.putters.popFirst()) |putter_node| {
17281793 const putter: *Put = @alignCast(@fieldParentPtr("node", putter_node));
1729 const copy_len = @min(putter.remaining.len, remaining.len);
1794 const copy_len = @min(putter.remaining.len, buffer.len - n);
17301795 assert(copy_len > 0);
1731 @memcpy(remaining[0..copy_len], putter.remaining[0..copy_len]);
1796 @memcpy(buffer[n..][0..copy_len], putter.remaining[0..copy_len]);
17321797 putter.remaining = putter.remaining[copy_len..];
1733 remaining = remaining[copy_len..];
1734 if (putter.remaining.len == 0) {
1798 putter.needed -|= copy_len;
1799 n += copy_len;
1800 if (putter.needed == 0) {
17351801 putter.condition.signal(io);
1736 if (remaining.len > 0) continue;
1737 } else q.putters.prepend(putter_node);
1738 assert(remaining.len == 0);
1739 q.fillRingBufferFromPutters(io);
1740 return buffer.len;
1802 } else {
1803 assert(n == buffer.len); // we didn't have enough space for the putter
1804 q.putters.prepend(putter_node);
1805 }
1806 if (n == buffer.len) {
1807 q.fillRingBufferFromPutters(io);
1808 return buffer.len;
1809 }
17411810 }
17421811
1743 // Both ring buffer and putters queue is empty.
1744 const total_filled = buffer.len - remaining.len;
1745 if (total_filled >= min) return total_filled;
1812 // No need to call `fillRingBufferFromPutters` from this point onwards,
1813 // because we emptied the ring buffer *and* the putter queue!
17461814
1747 var pending: Get = .{ .remaining = remaining, .condition = .{}, .node = .{} };
1815 // Don't block if we hit the target or if the queue is closed. Return how
1816 // many elements we could get immediately, unless the queue was closed and
1817 // empty, in which case report `error.Closed`.
1818 if (n == 0 and q.closed) return error.Closed;
1819 if (n >= target or q.closed) return n;
1820
1821 var pending: Get = .{
1822 .remaining = buffer[n..],
1823 .needed = target - n,
1824 .condition = .init,
1825 .node = .{},
1826 };
17481827 q.getters.append(&pending.node);
1749 defer if (pending.remaining.len > 0) q.getters.remove(&pending.node);
1750 while (pending.remaining.len > 0) if (uncancelable)
1751 pending.condition.waitUncancelable(io, &q.mutex)
1752 else
1753 try pending.condition.wait(io, &q.mutex);
1754 q.fillRingBufferFromPutters(io);
1755 return buffer.len;
1828 defer if (pending.needed > 0) q.getters.remove(&pending.node);
1829
1830 while (pending.needed > 0 and !q.closed) {
1831 if (uncancelable) {
1832 pending.condition.waitUncancelable(io, &q.mutex);
1833 continue;
1834 }
1835 pending.condition.wait(io, &q.mutex) catch |err| switch (err) {
1836 error.Canceled => if (pending.remaining.len == buffer.len) {
1837 // Canceled while waiting, and received no elements.
1838 return error.Canceled;
1839 } else {
1840 // Canceled while waiting, but received some elements, so report those first.
1841 io.recancel();
1842 return buffer.len - pending.remaining.len;
1843 },
1844 };
1845 }
1846 if (pending.remaining.len == buffer.len) {
1847 // The queue was closed while we were waiting. We received no elements.
1848 assert(q.closed);
1849 return error.Closed;
1850 }
1851 return buffer.len - pending.remaining.len;
17561852 }
17571853
17581854 /// Called when there is nonzero space available in the ring buffer and
......@@ -1768,7 +1864,8 @@ pub const TypeErasedQueue = struct {
17681864 @memcpy(slice[0..copy_len], putter.remaining[0..copy_len]);
17691865 q.len += copy_len;
17701866 putter.remaining = putter.remaining[copy_len..];
1771 if (putter.remaining.len == 0) {
1867 putter.needed -|= copy_len;
1868 if (putter.needed == 0) {
17721869 putter.condition.signal(io);
17731870 break;
17741871 }
......@@ -1791,59 +1888,101 @@ pub fn Queue(Elem: type) type {
17911888 return .{ .type_erased = .init(@ptrCast(buffer)) };
17921889 }
17931890
1794 /// Appends elements to the end of the queue. The function returns when
1795 /// at least `min` elements have been added to the buffer or sent
1796 /// directly to a consumer.
1891 pub fn close(q: *@This(), io: Io) void {
1892 q.type_erased.close(io);
1893 }
1894
1895 /// Appends elements to the end of the queue, potentially blocking if
1896 /// there is insufficient capacity. Returns when any one of the
1897 /// following conditions is satisfied:
1898 ///
1899 /// * At least `target` elements have been added to the queue
1900 /// * The queue is closed
1901 /// * The current task is canceled
1902 ///
1903 /// Returns how many of `elements` have been added to the queue, if any.
1904 /// If an error is returned, no elements have been added.
17971905 ///
1798 /// Returns how many elements have been added to the queue.
1906 /// If the queue is closed or the task is canceled, but some items were
1907 /// already added before the closure or cancelation, then `put` may
1908 /// return a number lower than `target`, in which case future calls are
1909 /// guaranteed to return `error.Canceled` or `error.Closed`.
17991910 ///
1800 /// Asserts that `elements.len >= min`.
1801 pub fn put(q: *@This(), io: Io, elements: []const Elem, min: usize) Cancelable!usize {
1802 return @divExact(try q.type_erased.put(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));
1911 /// A return value of 0 is only possible if `target` is 0, in which case
1912 /// the call is guaranteed to queue as many of `elements` as is possible
1913 /// *without* blocking.
1914 ///
1915 /// Asserts that `elements.len >= target`.
1916 pub fn put(q: *@This(), io: Io, elements: []const Elem, target: usize) (QueueClosedError || Cancelable)!usize {
1917 return @divExact(try q.type_erased.put(io, @ptrCast(elements), target * @sizeOf(Elem)), @sizeOf(Elem));
18031918 }
18041919
18051920 /// Same as `put` but blocks until all elements have been added to the queue.
1806 pub fn putAll(q: *@This(), io: Io, elements: []const Elem) Cancelable!void {
1807 assert(try q.put(io, elements, elements.len) == elements.len);
1921 ///
1922 /// If the queue is closed or canceled, `error.Closed` or `error.Canceled`
1923 /// is returned, and it is unspecified how many, if any, of `elements` were
1924 /// added to the queue prior to cancelation or closure.
1925 pub fn putAll(q: *@This(), io: Io, elements: []const Elem) (QueueClosedError || Cancelable)!void {
1926 const n = try q.put(io, elements, elements.len);
1927 if (n != elements.len) {
1928 _ = try q.put(io, elements[n..], elements.len - n);
1929 unreachable; // partial `put` implies queue was closed or we were canceled
1930 }
18081931 }
18091932
18101933 /// Same as `put`, except does not introduce a cancelation point.
18111934 ///
18121935 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1813 pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) usize {
1814 return @divExact(q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));
1936 pub fn putUncancelable(q: *@This(), io: Io, elements: []const Elem, min: usize) QueueClosedError!usize {
1937 return @divExact(try q.type_erased.putUncancelable(io, @ptrCast(elements), min * @sizeOf(Elem)), @sizeOf(Elem));
18151938 }
18161939
1817 pub fn putOne(q: *@This(), io: Io, item: Elem) Cancelable!void {
1940 /// Appends `item` to the end of the queue, blocking if the queue is full.
1941 pub fn putOne(q: *@This(), io: Io, item: Elem) (QueueClosedError || Cancelable)!void {
18181942 assert(try q.put(io, &.{item}, 1) == 1);
18191943 }
18201944
18211945 /// Same as `putOne`, except does not introduce a cancelation point.
18221946 ///
18231947 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1824 pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) void {
1825 assert(q.putUncancelable(io, &.{item}, 1) == 1);
1948 pub fn putOneUncancelable(q: *@This(), io: Io, item: Elem) QueueClosedError!void {
1949 assert(try q.putUncancelable(io, &.{item}, 1) == 1);
18261950 }
18271951
1828 /// Receives elements from the beginning of the queue. The function
1829 /// returns when at least `min` elements have been populated inside
1830 /// `buffer`.
1952 /// Receives elements from the beginning of the queue, potentially blocking
1953 /// if there are insufficient elements currently in the queue. Returns when
1954 /// any one of the following conditions is satisfied:
1955 ///
1956 /// * At least `target` elements have been received from the queue
1957 /// * The queue is closed and contains no buffered elements
1958 /// * The current task is canceled
1959 ///
1960 /// Returns how many elements of `buffer` have been populated, if any.
1961 /// If an error is returned, no elements have been populated.
1962 ///
1963 /// If the queue is closed or the task is canceled, but some items were
1964 /// already received before the closure or cancelation, then `get` may
1965 /// return a number lower than `target`, in which case future calls are
1966 /// guaranteed to return `error.Canceled` or `error.Closed`.
18311967 ///
1832 /// Returns how many elements of `buffer` have been populated.
1968 /// A return value of 0 is only possible if `target` is 0, in which case
1969 /// the call is guaranteed to fill as much of `buffer` as is possible
1970 /// *without* blocking.
18331971 ///
1834 /// Asserts that `buffer.len >= min`.
1835 pub fn get(q: *@This(), io: Io, buffer: []Elem, min: usize) Cancelable!usize {
1836 return @divExact(try q.type_erased.get(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem));
1972 /// Asserts that `buffer.len >= target`.
1973 pub fn get(q: *@This(), io: Io, buffer: []Elem, target: usize) (QueueClosedError || Cancelable)!usize {
1974 return @divExact(try q.type_erased.get(io, @ptrCast(buffer), target * @sizeOf(Elem)), @sizeOf(Elem));
18371975 }
18381976
18391977 /// Same as `get`, except does not introduce a cancelation point.
18401978 ///
18411979 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1842 pub fn getUncancelable(q: *@This(), io: Io, buffer: []Elem, min: usize) usize {
1980 pub fn getUncancelable(q: *@This(), io: Io, buffer: []Elem, min: usize) QueueClosedError!usize {
18431981 return @divExact(try q.type_erased.getUncancelable(io, @ptrCast(buffer), min * @sizeOf(Elem)), @sizeOf(Elem));
18441982 }
18451983
1846 pub fn getOne(q: *@This(), io: Io) Cancelable!Elem {
1984 /// Receives one element from the beginning of the queue, blocking if the queue is empty.
1985 pub fn getOne(q: *@This(), io: Io) (QueueClosedError || Cancelable)!Elem {
18471986 var buf: [1]Elem = undefined;
18481987 assert(try q.get(io, &buf, 1) == 1);
18491988 return buf[0];
......@@ -1852,9 +1991,9 @@ pub fn Queue(Elem: type) type {
18521991 /// Same as `getOne`, except does not introduce a cancelation point.
18531992 ///
18541993 /// For a description of cancelation and cancelation points, see `Future.cancel`.
1855 pub fn getOneUncancelable(q: *@This(), io: Io) Elem {
1994 pub fn getOneUncancelable(q: *@This(), io: Io) QueueClosedError!Elem {
18561995 var buf: [1]Elem = undefined;
1857 assert(q.getUncancelable(io, &buf, 1) == 1);
1996 assert(try q.getUncancelable(io, &buf, 1) == 1);
18581997 return buf[0];
18591998 }
18601999
lib/std/Io/Threaded.zig+19-14
......@@ -5795,11 +5795,13 @@ fn netLookup(
57955795 host_name: HostName,
57965796 resolved: *Io.Queue(HostName.LookupResult),
57975797 options: HostName.LookupOptions,
5798) void {
5798) net.HostName.LookupError!void {
57995799 const t: *Threaded = @ptrCast(@alignCast(userdata));
5800 const current_thread = Thread.getCurrent(t);
5801 const t_io = io(t);
5802 resolved.putOneUncancelable(t_io, .{ .end = netLookupFallible(t, current_thread, host_name, resolved, options) });
5800 defer resolved.close(io(t));
5801 netLookupFallible(t, host_name, resolved, options) catch |err| switch (err) {
5802 error.Closed => unreachable, // `resolved` must not be closed until `netLookup` returns
5803 else => |e| return e,
5804 };
58035805}
58045806
58055807fn netLookupUnavailable(
......@@ -5807,22 +5809,23 @@ fn netLookupUnavailable(
58075809 host_name: HostName,
58085810 resolved: *Io.Queue(HostName.LookupResult),
58095811 options: HostName.LookupOptions,
5810) void {
5812) net.HostName.LookupError!void {
58115813 _ = host_name;
58125814 _ = options;
58135815 const t: *Threaded = @ptrCast(@alignCast(userdata));
5814 const t_io = ioBasic(t);
5815 resolved.putOneUncancelable(t_io, .{ .end = error.NetworkDown });
5816 resolved.close(ioBasic(t));
5817 return error.NetworkDown;
58165818}
58175819
58185820fn netLookupFallible(
58195821 t: *Threaded,
5820 current_thread: *Thread,
58215822 host_name: HostName,
58225823 resolved: *Io.Queue(HostName.LookupResult),
58235824 options: HostName.LookupOptions,
5824) !void {
5825) (net.HostName.LookupError || Io.QueueClosedError)!void {
58255826 if (!have_networking) return error.NetworkDown;
5827
5828 const current_thread: *Thread = .getCurrent(t);
58265829 const t_io = io(t);
58275830 const name = host_name.bytes;
58285831 assert(name.len <= HostName.max_len);
......@@ -6363,7 +6366,7 @@ fn lookupDnsSearch(
63636366 host_name: HostName,
63646367 resolved: *Io.Queue(HostName.LookupResult),
63656368 options: HostName.LookupOptions,
6366) HostName.LookupError!void {
6369) (HostName.LookupError || Io.QueueClosedError)!void {
63676370 const t_io = io(t);
63686371 const rc = HostName.ResolvConf.init(t_io) catch return error.ResolvConfParseFailed;
63696372
......@@ -6407,7 +6410,7 @@ fn lookupDns(
64076410 rc: *const HostName.ResolvConf,
64086411 resolved: *Io.Queue(HostName.LookupResult),
64096412 options: HostName.LookupOptions,
6410) HostName.LookupError!void {
6413) (HostName.LookupError || Io.QueueClosedError)!void {
64116414 const t_io = io(t);
64126415 const family_records: [2]struct { af: IpAddress.Family, rr: HostName.DnsRecord } = .{
64136416 .{ .af = .ip6, .rr = .A },
......@@ -6621,8 +6624,10 @@ fn lookupHosts(
66216624 return error.DetectingNetworkConfigurationFailed;
66226625 },
66236626 },
6624 error.Canceled => |e| return e,
6625 error.UnknownHostName => |e| return e,
6627 error.Canceled,
6628 error.Closed,
6629 error.UnknownHostName,
6630 => |e| return e,
66266631 };
66276632}
66286633
......@@ -6632,7 +6637,7 @@ fn lookupHostsReader(
66326637 resolved: *Io.Queue(HostName.LookupResult),
66336638 options: HostName.LookupOptions,
66346639 reader: *Io.Reader,
6635) error{ ReadFailed, Canceled, UnknownHostName }!void {
6640) error{ ReadFailed, Canceled, UnknownHostName, Closed }!void {
66366641 const t_io = io(t);
66376642 var addresses_len: usize = 0;
66386643 var canonical_name: ?HostName = null;
lib/std/Io/net/HostName.zig+63-44
......@@ -82,19 +82,22 @@ pub const LookupError = error{
8282pub const LookupResult = union(enum) {
8383 address: IpAddress,
8484 canonical_name: HostName,
85 end: LookupError!void,
8685};
8786
88/// Adds any number of `IpAddress` into resolved, exactly one canonical_name,
89/// and then always finishes by adding one `LookupResult.end` entry.
87/// Adds any number of `LookupResult.address` into `resolved`, and exactly one
88/// `LookupResult.canonical_name`.
9089///
9190/// Guaranteed not to block if provided queue has capacity at least 16.
91///
92/// Closes `resolved` before return, even on error.
93///
94/// Asserts `resolved` is not closed until this call returns.
9295pub fn lookup(
9396 host_name: HostName,
9497 io: Io,
9598 resolved: *Io.Queue(LookupResult),
9699 options: LookupOptions,
97) void {
100) LookupError!void {
98101 return io.vtable.netLookup(io.userdata, host_name, resolved, options);
99102}
100103
......@@ -211,23 +214,25 @@ pub fn connect(
211214 port: u16,
212215 options: IpAddress.ConnectOptions,
213216) ConnectError!Stream {
214 var connect_many_buffer: [32]ConnectManyResult = undefined;
215 var connect_many_queue: Io.Queue(ConnectManyResult) = .init(&connect_many_buffer);
217 var connect_many_buffer: [32]IpAddress.ConnectError!Stream = undefined;
218 var connect_many_queue: Io.Queue(IpAddress.ConnectError!Stream) = .init(&connect_many_buffer);
216219
217220 var connect_many = io.async(connectMany, .{ host_name, io, port, &connect_many_queue, options });
218 var saw_end = false;
219221 defer {
220 connect_many.cancel(io);
221 if (!saw_end) while (true) switch (connect_many_queue.getOneUncancelable(io)) {
222 .connection => |loser| if (loser) |s| s.close(io) else |_| continue,
223 .end => break,
224 };
222 connect_many.cancel(io) catch {};
223 while (connect_many_queue.getOneUncancelable(io)) |loser| {
224 if (loser) |s| s.close(io) else |_| {}
225 } else |err| switch (err) {
226 error.Closed => {},
227 }
225228 }
226229
227 var aggregate_error: ConnectError = error.UnknownHostName;
230 var ip_connect_error: ?IpAddress.ConnectError = null;
228231
229 while (connect_many_queue.getOne(io)) |result| switch (result) {
230 .connection => |connection| if (connection) |stream| return stream else |err| switch (err) {
232 while (connect_many_queue.getOne(io)) |result| {
233 if (result) |stream| {
234 return stream;
235 } else |err| switch (err) {
231236 error.SystemResources,
232237 error.OptionUnsupported,
233238 error.ProcessFdQuotaExceeded,
......@@ -237,66 +242,80 @@ pub fn connect(
237242
238243 error.WouldBlock => return error.Unexpected,
239244
240 else => |e| aggregate_error = e,
241 },
242 .end => |end| {
243 saw_end = true;
244 try end;
245 return aggregate_error;
246 },
245 else => |e| ip_connect_error = e,
246 }
247247 } else |err| switch (err) {
248248 error.Canceled => |e| return e,
249 error.Closed => {
250 // There was no successful connection attempt. If there was a lookup error, return that.
251 try connect_many.await(io);
252 // Otherwise, return the error from a failed IP connection attempt.
253 return ip_connect_error orelse
254 return error.UnknownHostName;
255 },
249256 }
250257}
251258
252pub const ConnectManyResult = union(enum) {
253 connection: IpAddress.ConnectError!Stream,
254 end: ConnectError!void,
255};
256
257259/// Asynchronously establishes a connection to all IP addresses associated with
258260/// a host name, adding them to a results queue upon completion.
261///
262/// Closes `results` before return, even on error.
263///
264/// Asserts `results` is not closed until this call returns.
259265pub fn connectMany(
260266 host_name: HostName,
261267 io: Io,
262268 port: u16,
263 results: *Io.Queue(ConnectManyResult),
269 results: *Io.Queue(IpAddress.ConnectError!Stream),
264270 options: IpAddress.ConnectOptions,
265) void {
271) LookupError!void {
272 defer results.close(io);
273
266274 var canonical_name_buffer: [max_len]u8 = undefined;
267275 var lookup_buffer: [32]HostName.LookupResult = undefined;
268276 var lookup_queue: Io.Queue(LookupResult) = .init(&lookup_buffer);
269 var group: Io.Group = .init;
270 defer group.cancel(io);
271
272 group.async(io, lookup, .{ host_name, io, &lookup_queue, .{
277 var lookup_future = io.async(lookup, .{ host_name, io, &lookup_queue, .{
273278 .port = port,
274279 .canonical_name_buffer = &canonical_name_buffer,
275280 } });
281 defer lookup_future.cancel(io) catch {};
282
283 var group: Io.Group = .init;
284 defer group.cancel(io);
276285
277286 while (lookup_queue.getOne(io)) |dns_result| switch (dns_result) {
278287 .address => |address| group.async(io, enqueueConnection, .{ address, io, results, options }),
279288 .canonical_name => continue,
280 .end => |lookup_result| {
281 group.wait(io);
282 results.putOneUncancelable(io, .{ .end = lookup_result });
283 return;
284 },
285289 } else |err| switch (err) {
286 error.Canceled => |e| {
287 group.cancel(io);
288 results.putOneUncancelable(io, .{ .end = e });
290 error.Canceled => |e| return e,
291 error.Closed => {
292 group.wait(io);
293 return lookup_future.await(io);
289294 },
290295 }
291296}
292
293297fn enqueueConnection(
294298 address: IpAddress,
295299 io: Io,
296 queue: *Io.Queue(ConnectManyResult),
300 queue: *Io.Queue(IpAddress.ConnectError!Stream),
297301 options: IpAddress.ConnectOptions,
298302) void {
299 queue.putOneUncancelable(io, .{ .connection = address.connect(io, options) });
303 enqueueConnectionFallible(address, io, queue, options) catch |err| switch (err) {
304 error.Canceled => {},
305 };
306}
307fn enqueueConnectionFallible(
308 address: IpAddress,
309 io: Io,
310 queue: *Io.Queue(IpAddress.ConnectError!Stream),
311 options: IpAddress.ConnectOptions,
312) Io.Cancelable!void {
313 const result = address.connect(io, options);
314 errdefer if (result) |s| s.close(io) else |_| {};
315 queue.putOne(io, result) catch |err| switch (err) {
316 error.Closed => unreachable, // `queue` must not be closed
317 error.Canceled => |e| return e,
318 };
300319}
301320
302321pub const ResolvConf = struct {
lib/std/Io/net/test.zig+14-16
......@@ -129,7 +129,7 @@ test "resolve DNS" {
129129 var results_buffer: [32]net.HostName.LookupResult = undefined;
130130 var results: Io.Queue(net.HostName.LookupResult) = .init(&results_buffer);
131131
132 net.HostName.lookup(try .init("localhost"), io, &results, .{
132 try net.HostName.lookup(try .init("localhost"), io, &results, .{
133133 .port = 80,
134134 .canonical_name_buffer = &canonical_name_buffer,
135135 });
......@@ -142,11 +142,10 @@ test "resolve DNS" {
142142 addresses_found += 1;
143143 },
144144 .canonical_name => |canonical_name| try testing.expectEqualStrings("localhost", canonical_name.bytes),
145 .end => |end| {
146 try end;
147 break;
148 },
149 } else |err| return err;
145 } else |err| switch (err) {
146 error.Closed => {},
147 error.Canceled => |e| return e,
148 }
150149
151150 try testing.expect(addresses_found != 0);
152151 }
......@@ -161,20 +160,19 @@ test "resolve DNS" {
161160 net.HostName.lookup(try .init("example.com"), io, &results, .{
162161 .port = 80,
163162 .canonical_name_buffer = &canonical_name_buffer,
164 });
163 }) catch |err| switch (err) {
164 error.UnknownHostName => return error.SkipZigTest,
165 error.NameServerFailure => return error.SkipZigTest,
166 else => |e| return e,
167 };
165168
166169 while (results.getOne(io)) |result| switch (result) {
167170 .address => {},
168171 .canonical_name => {},
169 .end => |end| {
170 end catch |err| switch (err) {
171 error.UnknownHostName => return error.SkipZigTest,
172 error.NameServerFailure => return error.SkipZigTest,
173 else => return err,
174 };
175 break;
176 },
177 } else |err| return err;
172 } else |err| switch (err) {
173 error.Closed => {},
174 error.Canceled => |e| return e,
175 }
178176 }
179177}
180178
lib/std/Io/test.zig+53-8
......@@ -209,10 +209,10 @@ test "select" {
209209 return;
210210 },
211211 };
212 defer if (get_a.cancel(io)) |_| {} else |_| @panic("fail");
212 defer _ = get_a.cancel(io) catch {};
213213
214214 var get_b = try io.concurrent(Io.Queue(u8).getOne, .{ &queue, io });
215 defer if (get_b.cancel(io)) |_| {} else |_| @panic("fail");
215 defer _ = get_b.cancel(io) catch {};
216216
217217 var timeout = io.async(Io.sleep, .{ io, .fromMilliseconds(1), .awake });
218218 defer timeout.cancel(io) catch {};
......@@ -225,12 +225,9 @@ test "select" {
225225 .get_a => return error.TestFailure,
226226 .get_b => return error.TestFailure,
227227 .timeout => {
228 // Unblock the queues to avoid making this unit test depend on
229 // cancellation.
230 queue.putOneUncancelable(io, 1);
231 queue.putOneUncancelable(io, 1);
232 try testing.expectEqual(1, try get_a.await(io));
233 try testing.expectEqual(1, try get_b.await(io));
228 queue.close(io);
229 try testing.expectError(error.Closed, get_a.await(io));
230 try testing.expectError(error.Closed, get_b.await(io));
234231 },
235232 }
236233}
......@@ -256,6 +253,54 @@ test "Queue" {
256253 try testQueue(5);
257254}
258255
256test "Queue.close single-threaded" {
257 const io = std.testing.io;
258
259 var buf: [10]u8 = undefined;
260 var queue: Io.Queue(u8) = .init(&buf);
261
262 try queue.putAll(io, &.{ 0, 1, 2, 3, 4, 5, 6 });
263 try expectEqual(3, try queue.put(io, &.{ 7, 8, 9, 10 }, 0)); // there is capacity for 3 more items
264
265 var get_buf: [4]u8 = undefined;
266
267 // Receive some elements before closing
268 try expectEqual(4, try queue.get(io, &get_buf, 0));
269 try expectEqual(0, get_buf[0]);
270 try expectEqual(1, get_buf[1]);
271 try expectEqual(2, get_buf[2]);
272 try expectEqual(3, get_buf[3]);
273 try expectEqual(4, try queue.getOne(io));
274
275 // ...and add a couple more now there's space
276 try queue.putAll(io, &.{ 20, 21 });
277
278 queue.close(io);
279
280 // Receive more elements *after* closing
281 try expectEqual(4, try queue.get(io, &get_buf, 0));
282 try expectEqual(5, get_buf[0]);
283 try expectEqual(6, get_buf[1]);
284 try expectEqual(7, get_buf[2]);
285 try expectEqual(8, get_buf[3]);
286 try expectEqual(9, try queue.getOne(io));
287
288 // Cannot put anything while closed, even if the buffer has space
289 try expectError(error.Closed, queue.putOne(io, 100));
290 try expectError(error.Closed, queue.putAll(io, &.{ 101, 102 }));
291 try expectError(error.Closed, queue.putUncancelable(io, &.{ 103, 104 }, 0));
292
293 // Even if we ask for 3 items, the queue is closed, so we only get the last 2
294 try expectEqual(2, try queue.get(io, &get_buf, 4));
295 try expectEqual(20, get_buf[0]);
296 try expectEqual(21, get_buf[1]);
297
298 // The queue is now empty, so `get` should return `error.Closed` too
299 try expectError(error.Closed, queue.getOne(io));
300 try expectError(error.Closed, queue.get(io, &get_buf, 0));
301 try expectError(error.Closed, queue.putUncancelable(io, &get_buf, 2));
302}
303
259304test "Event" {
260305 const global = struct {
261306 fn waitAndRead(io: Io, event: *Io.Event, ptr: *const u32) Io.Cancelable!u32 {