LCOV - code coverage report
Current view: top level - capy - task.hpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 95 95
Test Date: 2026-11-06 16:57:03 Functions: 93.3 % 1663 1552 111

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
       3                 : // Copyright (c) 2026 Michael Vandeberg
       4                 : //
       5                 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       6                 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       7                 : //
       8                 : // Official repository: https://github.com/cppalliance/capy
       9                 : //
      10                 : 
      11                 : #ifndef BOOST_CAPY_TASK_HPP
      12                 : #define BOOST_CAPY_TASK_HPP
      13                 : 
      14                 : #include <boost/capy/detail/config.hpp>
      15                 : #include <boost/capy/concept/executor.hpp>
      16                 : #include <boost/capy/concept/context_awaitable.hpp>
      17                 : #include <boost/capy/ex/context_awaitable_promise_base.hpp>
      18                 : #include <boost/capy/ex/await_context.hpp>
      19                 : #include <boost/capy/ex/frame_allocator.hpp>
      20                 : #include <boost/capy/detail/await_suspend_helper.hpp>
      21                 : #include <boost/capy/results.hpp>
      22                 : 
      23                 : #include <exception>
      24                 : #include <optional>
      25                 : #include <type_traits>
      26                 : #include <utility>
      27                 : #include <variant>
      28                 : 
      29                 : namespace boost {
      30                 : namespace capy {
      31                 : 
      32                 : namespace detail {
      33                 : 
      34                 : // Helper base for result storage and return_void/return_value
      35                 : template<typename T>
      36                 : struct task_return_base
      37                 : {
      38                 :     std::optional<T> result_;
      39                 : 
      40 HIT         899 :     void return_value(T value)
      41                 :     {
      42             899 :         result_ = std::move(value);
      43             899 :     }
      44                 : 
      45             284 :     T&& result() noexcept
      46                 :     {
      47             284 :         return std::move(*result_);
      48                 :     }
      49                 : };
      50                 : 
      51                 : template<>
      52                 : struct task_return_base<void>
      53                 : {
      54            4157 :     void return_void()
      55                 :     {
      56            4157 :     }
      57                 : };
      58                 : 
      59                 : } // namespace detail
      60                 : 
      61                 : /** Defers a coroutine body until awaited, then runs it inline on the caller's thread.
      62                 : 
      63                 :     Use `task<T>` as the return type for coroutines that perform I/O
      64                 :     and return a value of type `T`. The coroutine body does not start
      65                 :     executing until the task is awaited, enabling efficient composition
      66                 :     without unnecessary eager execution.
      67                 : 
      68                 :     The task participates in the I/O awaitable protocol: when awaited,
      69                 :     it receives the caller's executor and stop token, propagating them
      70                 :     to nested `co_await` expressions. This enables cancellation and
      71                 :     proper completion dispatch across executor boundaries.
      72                 : 
      73                 :     @par Await-effects
      74                 : 
      75                 :     Let `t` be a `task<T>`. `co_await t` always suspends the awaiting
      76                 :     coroutine, then transfers control directly into the task's coroutine
      77                 :     body on the current thread; no executor operation is posted. The task
      78                 :     records the caller's environment (executor, stop token, and frame
      79                 :     allocator) by pointer rather than copying it. It propagates that
      80                 :     environment to every `co_await` inside the body.
      81                 : 
      82                 :     The body runs until it returns or exits via an exception. Control
      83                 :     then transfers directly back to the awaiting coroutine, again
      84                 :     without an executor operation.
      85                 : 
      86                 :     `task` never inspects the stop token; it only propagates it. A task
      87                 :     body observes a stop request through the results of the operations it
      88                 :     awaits, or by reading the token itself.
      89                 : 
      90                 :     @par Await-returns
      91                 :     The value the body passed to `co_return`, moved out of the task, or
      92                 :     nothing when `T` is `void`.
      93                 : 
      94                 :     If the body exits via an unhandled exception, that exception is
      95                 :     rethrown instead.
      96                 : 
      97                 :     @par Await-postcondition
      98                 :     The task's coroutine has run to completion and is suspended at its
      99                 :     final suspend point. The task still owns the frame, but not the
     100                 :     result: the await moves it out, so a task must not be awaited twice.
     101                 : 
     102                 :     @par Thread Safety
     103                 :     Distinct objects: Safe.
     104                 :     Shared objects: Unsafe.
     105                 : 
     106                 :     @par Example
     107                 : 
     108                 :     @par !example example
     109                 : 
     110                 : 
     111                 :     @tparam T The result type. Use `task<>` for `task<void>`.
     112                 : 
     113                 :     @see @ref ContextRunnable, @ref ContextAwaitable, @ref run, @ref run_async
     114                 : */
     115                 : template<typename T = void>
     116                 : struct [[nodiscard]] BOOST_CAPY_CORO_AWAIT_ELIDABLE
     117                 :     task
     118                 : {
     119                 :     /** Stores `task<T>`'s result and joins the I/O awaitable protocol via `context_awaitable_promise_base`.
     120                 : 
     121                 :         This is the promise object the compiler associates with a
     122                 :         `task<T>` coroutine. It satisfies the coroutine promise
     123                 :         requirements and participates in the I/O awaitable protocol via
     124                 :         @ref context_awaitable_promise_base. It is part of the coroutine
     125                 :         machinery and is not intended to be used directly by callers.
     126                 : 
     127                 :         Result storage and `return_value`/`return_void` are provided by
     128                 :         `detail::task_return_base<T>`.
     129                 : 
     130                 :         @see @ref context_awaitable_promise_base, @ref ContextRunnable
     131                 :     */
     132                 :     struct promise_type
     133                 :         : context_awaitable_promise_base<promise_type>
     134                 :         , detail::task_return_base<T>
     135                 :     {
     136                 :     private:
     137                 :         friend task;
     138                 :         union { std::exception_ptr ep_; };
     139                 :         bool has_ep_;
     140                 : 
     141                 :     public:
     142                 :         /// Construct the promise with no stored exception.
     143          109250 :         promise_type() noexcept
     144          109250 :             : has_ep_(false)
     145                 :         {
     146          109250 :         }
     147                 : 
     148                 :         /// Destroy the promise, releasing any stored exception.
     149          109250 :         ~promise_type()
     150                 :         {
     151          109250 :             if(has_ep_)
     152             485 :                 ep_.~exception_ptr();
     153          109250 :         }
     154                 : 
     155                 :         /** Return the exception captured by the coroutine body, if any.
     156                 : 
     157                 :             @return The stored exception, or a null `std::exception_ptr`
     158                 :             if the coroutine did not exit via an unhandled exception.
     159                 :         */
     160            5032 :         std::exception_ptr exception() const noexcept
     161                 :         {
     162            5032 :             if(has_ep_)
     163             726 :                 return ep_;
     164            4306 :             return {};
     165                 :         }
     166                 : 
     167                 :         /** Return the owning `task` for this coroutine.
     168                 : 
     169                 :             Called by the compiler to produce the object returned to the
     170                 :             caller when the coroutine is created.
     171                 : 
     172                 :             @return A `task` owning the coroutine frame.
     173                 :         */
     174          109250 :         task get_return_object()
     175                 :         {
     176          109250 :             return task{std::coroutine_handle<promise_type>::from_promise(*this)};
     177                 :         }
     178                 : 
     179                 :         /** Return the initial-suspend awaiter.
     180                 : 
     181                 :             The coroutine always suspends at the initial suspend point,
     182                 :             so the body does not start until the task is awaited. When the
     183                 :             body is resumed, the awaiter restores the thread-local frame
     184                 :             allocator from the stored environment.
     185                 : 
     186                 :             @return An awaiter that suspends unconditionally.
     187                 :         */
     188          109250 :         auto initial_suspend() noexcept
     189                 :         {
     190                 :             struct awaiter
     191                 :             {
     192                 :                 promise_type* p_;
     193                 : 
     194          109250 :                 bool await_ready() const noexcept
     195                 :                 {
     196          109250 :                     return false;
     197                 :                 }
     198                 : 
     199          109250 :                 void await_suspend(std::coroutine_handle<>) const noexcept
     200                 :                 {
     201          109250 :                 }
     202                 : 
     203          108167 :                 void await_resume() const noexcept
     204                 :                 {
     205                 :                     // Restore TLS when body starts executing
     206          108167 :                     set_current_frame_allocator(p_->environment()->frame_allocator);
     207          108167 :                 }
     208                 :             };
     209          109250 :             return awaiter{this};
     210                 :         }
     211                 : 
     212                 :         /** Return the final-suspend awaiter.
     213                 : 
     214                 :             The coroutine always suspends at the final suspend point. The
     215                 :             awaiter's `await_suspend` performs symmetric transfer to the
     216                 :             stored continuation (consuming it), resuming the awaiting
     217                 :             coroutine.
     218                 : 
     219                 :             @return An awaiter that suspends and transfers to the
     220                 :             continuation.
     221                 :         */
     222            5541 :         auto final_suspend() noexcept
     223                 :         {
     224                 :             struct awaiter
     225                 :             {
     226                 :                 promise_type* p_;
     227                 : 
     228            5541 :                 bool await_ready() const noexcept
     229                 :                 {
     230            5541 :                     return false;
     231                 :                 }
     232                 : 
     233            5541 :                 std::coroutine_handle<> await_suspend(std::coroutine_handle<>) const noexcept
     234                 :                 {
     235            5541 :                     return p_->continuation();
     236                 :                 }
     237                 : 
     238                 :                 void await_resume() const noexcept {} // LCOV_EXCL_LINE final_suspend awaiter, never resumed
     239                 :             };
     240            5541 :             return awaiter{this};
     241                 :         }
     242                 : 
     243                 :         /** Capture the in-flight exception from the coroutine body.
     244                 : 
     245                 :             Called by the compiler when the coroutine body exits via an
     246                 :             unhandled exception. The captured exception is rethrown when
     247                 :             the task is awaited.
     248                 :         */
     249             485 :         void unhandled_exception() noexcept
     250                 :         {
     251             485 :             new (&ep_) std::exception_ptr(std::current_exception());
     252             485 :             has_ep_ = true;
     253             485 :         }
     254                 : 
     255                 :         /** Awaiter wrapping a nested `co_await` of an @ref ContextAwaitable.
     256                 : 
     257                 :             Forwards the environment to the inner awaitable's
     258                 :             environment-taking `await_suspend` and restores the
     259                 :             thread-local frame allocator before the body resumes.
     260                 : 
     261                 :             @tparam Awaitable The awaitable being transformed.
     262                 :         */
     263                 :         template<class Awaitable>
     264                 :         struct transform_awaiter
     265                 :         {
     266                 :             /// The wrapped awaitable, decayed and stored by value.
     267                 :             std::decay_t<Awaitable> a_;
     268                 : 
     269                 :             /// The promise of the coroutine performing the `co_await`.
     270                 :             promise_type* p_;
     271                 : 
     272                 :             /** Report whether the wrapped awaitable is already complete.
     273                 : 
     274                 :                 Where `BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND` is on,
     275                 :                 this always returns `false`, and @ref await_suspend asks
     276                 :                 the wrapped awaitable instead. See
     277                 :                 `detail::await_ready_then_suspend`.
     278                 : 
     279                 :                 @return The wrapped awaitable's own `await_ready` result:
     280                 :                 `true` if no suspension is needed. Always `false` where
     281                 :                 the workaround is on.
     282                 :             */
     283          108233 :             bool await_ready() noexcept
     284                 :             {
     285                 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
     286                 :                 return false;
     287                 : #else
     288          108233 :                 return a_.await_ready();
     289                 : #endif
     290                 :             }
     291                 : 
     292                 :             /** Restore the frame allocator, then resume the wrapped
     293                 :                 awaitable.
     294                 : 
     295                 :                 Reinstalls the thread-local frame allocator from the stored
     296                 :                 environment before the body continues. This is needed
     297                 :                 because the resumption may arrive on a different thread
     298                 :                 than the one that suspended.
     299                 : 
     300                 :                 @return The wrapped awaitable's await-result, forwarded
     301                 :                 unchanged.
     302                 :             */
     303            5607 :             decltype(auto) await_resume()
     304                 :             {
     305                 :                 // Restore TLS before body resumes
     306            5607 :                 set_current_frame_allocator(p_->environment()->frame_allocator);
     307            5607 :                 return a_.await_resume();
     308                 :             }
     309                 : 
     310                 :             /** Suspend by calling the wrapped awaitable with the
     311                 :                 environment.
     312                 : 
     313                 :                 This is the plain `await_suspend` the compiler calls for the
     314                 :                 nested `co_await`. It forwards to the wrapped awaitable's
     315                 :                 @ref ContextAwaitable overload, supplying the promise's stored
     316                 :                 environment as the second argument. It then hands back
     317                 :                 that call's result unchanged, so the wrapped awaitable's
     318                 :                 suspension decision, whatever form it takes, is preserved.
     319                 : 
     320                 :                 @param h The coroutine performing the `co_await`.
     321                 : 
     322                 :                 @return Whatever the wrapped awaitable's `await_suspend`
     323                 :                 returns. When that is a `std::coroutine_handle<>`, the
     324                 :                 handle is routed through `detail::symmetric_transfer`.
     325                 :                 Where `BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND` is on,
     326                 :                 this function instead returns `bool` from
     327                 :                 `detail::await_ready_then_suspend`. It first asks the
     328                 :                 wrapped awaitable's `await_ready`. When that reports
     329                 :                 ready, it returns `false` to continue without
     330                 :                 suspending. Otherwise it resumes any returned handle on
     331                 :                 the current stack and returns `true`. Where the
     332                 :                 workaround is off, a returned handle is passed back
     333                 :                 unchanged for symmetric transfer.
     334                 :             */
     335                 :             template<class Promise>
     336          107608 :             auto await_suspend(std::coroutine_handle<Promise> h) noexcept
     337                 :             {
     338                 : #if BOOST_CAPY_SYMMETRIC_TRANSFER_WORKAROUND
     339                 :                 return detail::await_ready_then_suspend(a_, h, p_->environment());
     340                 : #else
     341                 :                 using R = decltype(a_.await_suspend(h, p_->environment()));
     342                 :                 if constexpr (std::is_same_v<R, std::coroutine_handle<>>)
     343          106570 :                     return detail::symmetric_transfer(a_.await_suspend(h, p_->environment()));
     344                 :                 else
     345            1038 :                     return a_.await_suspend(h, p_->environment());
     346                 : #endif
     347                 :             }
     348                 :         };
     349                 : 
     350                 :         /** Transform a nested awaitable before `co_await`.
     351                 : 
     352                 :             Wraps an @ref ContextAwaitable in a @ref transform_awaiter so the
     353                 :             coroutine's environment is propagated into it. A diagnostic
     354                 :             is emitted if the awaitable does not satisfy @ref ContextAwaitable.
     355                 : 
     356                 :             @param a The awaitable expression from `co_await a`.
     357                 : 
     358                 :             @return A @ref transform_awaiter wrapping `a`.
     359                 :         */
     360                 :         template<class Awaitable>
     361          108233 :         auto transform_awaitable(Awaitable&& a)
     362                 :         {
     363                 :             using A = std::decay_t<Awaitable>;
     364                 :             if constexpr (ContextAwaitable<A>)
     365                 :             {
     366                 :                 return transform_awaiter<Awaitable>{
     367          213949 :                     std::forward<Awaitable>(a), this};
     368                 :             }
     369                 :             else
     370                 :             {
     371                 :                 static_assert(ContextAwaitable<A>, "requires ContextAwaitable");
     372                 :             }
     373          105716 :         }
     374                 :     };
     375                 : 
     376                 :     /** Handle to the owned coroutine frame.
     377                 : 
     378                 :         Null when the task is empty (for example after a move or after
     379                 :         @ref release). Prefer @ref handle to read this; the member is
     380                 :         public for use by the coroutine machinery.
     381                 :     */
     382                 :     std::coroutine_handle<promise_type> h_;
     383                 : 
     384                 :     /** Destroy the task and its coroutine frame if owned.
     385                 : 
     386                 :         @par Preconditions
     387                 :         The frame is not lent: no parent is suspended awaiting it,
     388                 :         unless its chain is being unwound after an @ref abandon.
     389                 :         Destroying a lent frame otherwise calls `std::terminate`; end
     390                 :         a running chain by requesting stop, or by destroying its
     391                 :         execution context.
     392                 :     */
     393          113324 :     ~task()
     394                 :     {
     395          113324 :         destroy_frame();
     396          113324 :     }
     397                 : 
     398                 :     /** Report whether the awaited task is already complete.
     399                 : 
     400                 :         Always returns `false`; a task is lazy and has not started when
     401                 :         it is awaited, so the awaiting coroutine always suspends.
     402                 : 
     403                 :         @return `false`.
     404                 :     */
     405            2108 :     bool await_ready() const noexcept
     406                 :     {
     407            2108 :         return false;
     408                 :     }
     409                 : 
     410                 :     /** Return the task's result, rethrowing any captured exception.
     411                 : 
     412                 :         If the coroutine body exited via an unhandled exception, that
     413                 :         exception is rethrown here. Otherwise the result is returned by
     414                 :         move (for `task<T>`) or nothing is returned (for `task<void>`).
     415                 : 
     416                 :         @return The result value for non-void `T`; otherwise `void`.
     417                 : 
     418                 :         @throws The exception captured by the coroutine body, if any.
     419                 : 
     420                 :         @note Discarding a `results` silently drops the error
     421                 :         code, so that overload is marked `[[nodiscard]]`.
     422                 :     */
     423             566 :     [[nodiscard]] auto await_resume()
     424                 :         requires detail::is_results_v<T>
     425                 :     {
     426             566 :         lent_ = false;
     427             566 :         if(h_.promise().has_ep_)
     428             104 :             std::rethrow_exception(h_.promise().ep_);
     429             462 :         return std::move(*h_.promise().result_);
     430                 :     }
     431                 : 
     432             243 :     auto await_resume()
     433                 :         requires (! detail::is_results_v<T>)
     434                 :     {
     435             243 :         lent_ = false;
     436             243 :         if(h_.promise().has_ep_)
     437              17 :             std::rethrow_exception(h_.promise().ep_);
     438                 :         if constexpr (! std::is_void_v<T>)
     439             151 :             return std::move(*h_.promise().result_);
     440                 :         else
     441              75 :             return;
     442                 :     }
     443                 : 
     444                 :     /** Start the task with the awaiting coroutine's context.
     445                 : 
     446                 :         Stores `cont` as the continuation to resume on completion.
     447                 :         Stores `env` as the execution environment propagated to nested
     448                 :         `co_await` expressions. Then transfers control into the task's
     449                 :         coroutine body via the returned handle.
     450                 : 
     451                 :         @param cont The awaiting coroutine to resume when the task
     452                 :         completes.
     453                 : 
     454                 :         @param env The execution environment (executor, stop token, and
     455                 :         frame allocator). It must outlive the task.
     456                 : 
     457                 :         @return The task's coroutine handle, for symmetric transfer.
     458                 :     */
     459            2005 :     std::coroutine_handle<> await_suspend(std::coroutine_handle<> cont, await_context const* env)
     460                 :     {
     461            2005 :         h_.promise().set_continuation(cont);
     462            2005 :         h_.promise().set_environment(env);
     463            2005 :         lent_ = true;
     464            2005 :         return h_;
     465                 :     }
     466                 : 
     467                 :     /** Return the coroutine handle.
     468                 : 
     469                 :         @note Do not call `destroy()` on the returned handle while the
     470                 :         task is being awaited: the awaiting frame still holds it and
     471                 :         destroys it again, which has undefined behavior. A holder that
     472                 :         gives the coroutine up passes its continuation to @ref abandon
     473                 :         instead. (Destroying or
     474                 :         move-assigning over the task object itself while its frame is
     475                 :         lent is a different operation, and calls `std::terminate`.)
     476                 :         The task's lifetime is normally managed by `run_async`, `run`,
     477                 :         or the awaiting parent. For cooperative cancellation, use
     478                 :         `std::stop_token`.
     479                 : 
     480                 :         @note The operation, queue or primitive holding a suspended
     481                 :         coroutine gives it up with @ref abandon, through the
     482                 :         continuation it keeps, never by destroying a handle.
     483                 :         `handle()` belongs to the owning task object.
     484                 : 
     485                 :         @return The coroutine handle.
     486                 :     */
     487          107274 :     std::coroutine_handle<promise_type> handle() const noexcept
     488                 :     {
     489          107274 :         return h_;
     490                 :     }
     491                 : 
     492                 :     /** Release ownership of the coroutine frame.
     493                 : 
     494                 :         After calling this, destroying the task does not destroy the
     495                 :         coroutine frame. The caller becomes responsible for the frame's
     496                 :         lifetime.
     497                 : 
     498                 :         @note The caller may call `destroy()` on the released handle
     499                 :         only when the task has not started or has fully completed.
     500                 :         Destroying the released handle of a task that is being
     501                 :         awaited is not supported and has undefined behavior.
     502                 : 
     503                 :         @par Postconditions
     504                 :         `handle()` returns a null handle. Callers needing the
     505                 :         original handle must save it, via @ref handle, before
     506                 :         calling this.
     507                 :     */
     508          107131 :     void release() noexcept
     509                 :     {
     510          107131 :         h_ = nullptr;
     511          107131 :     }
     512                 : 
     513                 :     /** Copy construction is disabled; a task uniquely owns its frame.
     514                 : 
     515                 :         @param other The task that would be copied.
     516                 :     */
     517                 :     task(task const& other) = delete;
     518                 : 
     519                 :     /** Copy assignment is disabled; a task uniquely owns its frame.
     520                 : 
     521                 :         @param other The task that would be assigned from.
     522                 : 
     523                 :         @return A reference to `*this`.
     524                 :     */
     525                 :     task& operator=(task const& other) = delete;
     526                 : 
     527                 :     /** Construct by moving, transferring ownership of the frame.
     528                 : 
     529                 :         @par Postconditions
     530                 :         `other` is empty and must not be awaited.
     531                 : 
     532                 :         @param other The task to move from.
     533                 :     */
     534            4074 :     task(task&& other) noexcept
     535            4074 :         : h_(std::exchange(other.h_, nullptr))
     536                 :     {
     537            4074 :     }
     538                 : 
     539                 :     /** Assign by moving, transferring ownership of the frame.
     540                 : 
     541                 :         If this task already owns a coroutine frame, that frame is
     542                 :         destroyed first. Self-assignment is a no-op.
     543                 : 
     544                 :         @par Preconditions
     545                 :         The frame being replaced is not lent: no parent is suspended
     546                 :         awaiting it. Replacing a lent frame calls `std::terminate`.
     547                 : 
     548                 :         @par Postconditions
     549                 :         `other` is empty and must not be awaited.
     550                 : 
     551                 :         @param other The task to move from.
     552                 : 
     553                 :         @return A reference to `*this`.
     554                 :     */
     555               1 :     task& operator=(task&& other) noexcept
     556                 :     {
     557               1 :         if(this != &other)
     558                 :         {
     559               1 :             destroy_frame();
     560               1 :             h_ = std::exchange(other.h_, nullptr);
     561                 :         }
     562               1 :         return *this;
     563                 :     }
     564                 : 
     565                 : private:
     566                 :     // Set while a parent is suspended awaiting this task.
     567                 :     bool lent_ = false;
     568                 : 
     569          109250 :     explicit task(std::coroutine_handle<promise_type> h)
     570          109250 :         : h_(h)
     571                 :     {
     572          109250 :     }
     573                 : 
     574          113325 :     void destroy_frame() noexcept
     575                 :     {
     576          113325 :         if(!h_)
     577          111206 :             return;
     578            2119 :         if(lent_ && !h_.done())
     579                 :         {
     580                 :             // Only the unwind of an abandoned chain destroys a frame
     581                 :             // that is still awaited.
     582            1277 :             auto* owner = h_.promise().environment()->owner;
     583            1277 :             if(!owner || !owner->abandoned())
     584                 :                 std::terminate(); // LCOV_EXCL_LINE reached only in a death test's child process
     585                 :         }
     586            2119 :         std::exchange(h_, nullptr).destroy();
     587            2119 :         lent_ = false;
     588                 :     }
     589                 : };
     590                 : 
     591                 : } // namespace capy
     592                 : } // namespace boost
     593                 : 
     594                 : #endif
        

Generated by: LCOV version 2.3