Skip to content

Reference count never goes to 0 with complex effect (async)? #854

@timbertson

Description

@timbertson

I found this because I was working on some async code in koka-community/uv, where the relevant resource gets uv_close()d when its last reference is dropped, rather than requiring the programmer remember to close everything explicitly.

Basically, it seems that when async code is involved the reference count of an object (specifically a wrapped C pointer) doesn't ever drop to 0.

using:

I wrote the following test module to demonstrate the issue:

import std/test
import std/async/async
import std/num/int32
import std/time/duration
import uv/event-loop

extern create-box(): io-noexn any
  c inline "kk_cptr_raw_box(&kk_free_fun, kk_malloc(sizeof(int), _ctx), _ctx)"

extern get-refcount(^box: any): io-noexn int32
  c inline "kk_block_refcount(kk_box_to_ptr(box, _ctx))"

fun get-refcount-copy(box: any)
  box.get-refcount

fun get-refcount-async(^box: any)
  val count = box.get-refcount
  wait(1.milli-seconds)
  count

fun main()
  with default-event-loop
  with async/handle
  run-tests(suite)

fun suite()
  effectful-test("single-use")
    expect(0)
      val box = create-box()
      get-refcount(box).int
    
  effectful-test("get refcount from a copy")
    expect((1, 0))
      val box = create-box()
      val from-copy = box.get-refcount-copy().int
      val from-ref = box.get-refcount().int
      (from-copy, from-ref)

  effectful-test("get refcount from a copy with async effect")
    expect((1, 0, 0))
      val box = create-box()
      val from-copy = box.get-refcount-copy().int
      val from-ref-async = box.get-refcount-async().int
      val from-ref-after-async = box.get-refcount().int
      (from-copy, from-ref-async, from-ref-after-async)

It prints:

single-use
- ok
get refcount from a copy
- ok
get refcount from a copy with async effect
Expectation failed (ref-test:41)
expected: (1,0,0)
     got: (1,2,1)
- failed
    # ref-test:40

1 failures, 2 successes

Failed tests:
 - get refcount from a copy with async effect
error  : command failed (exit code 1)

The second and third tests are essentially the same, except the third uses an async effect.

Here's a simpler reproduction without using the test module:

import std/async/async
import std/num/int32
import std/time/duration
import uv/event-loop
import std/core/debug

extern create-box(): io-noexn any
  c inline "kk_cptr_raw_box(&kk_free_fun, kk_malloc(sizeof(int), _ctx), _ctx)"

extern get-refcount(^box: any): io-noexn int32
  c inline "kk_block_refcount(kk_box_to_ptr(box, _ctx))"

fun get-refcount-copy(box: any)
  box.get-refcount

fun get-refcount-async(^box: any)
  val count = box.get-refcount
  wait(1.milli-seconds)
  count

fun print-result(action: () -> <exn,console,div|e> a, ?a/show: (a) -> div string): <console,div|e> ()
  val result = try(action)
  match result
    Ok(result) -> println(result.show)
    Error(err) ->
      println(err.show)
      impossible("error")

fun main()
  with default-event-loop
  with async/handle
  print-result
    val box = create-box()
    box.get-refcount().int

  print-result
    val box = create-box()
    box.get-refcount-async().int

This prints:

0
1

It seems to cap out at two references if I change the second block to run many async actions:

    val box = create-box()
    val a = box.get-refcount-async().int
    val b = box.get-refcount-async().int
    val c = box.get-refcount-async().int
    (a,b,c)

-> prints (1,2,2,2)

This could be somehow caused by the async implementation in C (in which case it's just a problem for #852), but it seems more likely this is a bug with how the compiler handles the various raw ctl operations in the std/async/async effect.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions