发现了一个关于SendEvent和Task之间可能存在的冲突bug。

一句话总结:await client.SendEvent()调用后面必须紧跟一个print语句,否则则会导致yield return new WaitUntil(() => task.IsCompleted);的task一直是未完成的状态,卡在这里无法继续执行。

补充信息:
如果打开消息日志,bug就会消失(task会完成),如果注释掉下面两行消息日志的代码,bug就会出现。

    LeanCloud.Common.Logger.LogDelegate = (level, info) =>
    { Debug.Log($"[{level}] {info}"); };

详细情况:
主要用了球球大作战demo的机制(master用Coroutine每秒执行一次倒计时),在某些时间点会执行一些计算并SetProperties或SendEvents,由于这些是异步任务所以使用了WaitUntil(() => task.IsCompleted)的方式中断Coroutine。
代码如下:

IEnumerator CountingCoroutine()
{
    while (true)
    {
        var client = LeanCloudUtils.GetClient();
        var props = client.Room.CustomProperties;
        if (props.GetByte(Keys.STATE) == Constants.STATE_PLAY)
        {
            var countDown = props.GetInt(Keys.COUNTDOWN);
            countDown -= 1;
            if (countDown <= 0)
            {
                var task = ChangeTurn();
                print("================= waiting ==================");
                yield return new WaitUntil(() => task.IsCompleted);
                print("================= completed ==================");
            }
            else
            {
                var newProps = new PlayObject {
                    { Keys.COUNTDOWN, countDown }
                };
                client.Room.SetCustomProperties(newProps);
            }
        }
        yield return new WaitForSeconds(Constants.COUNTING_INTERVAL);
    }
}

async Task ChangeTurn()
{
    print("更换回合");
    var client = LeanCloudUtils.GetClient();
    var props = client.Room.CustomProperties;
    var oldActorId = props.GetInt(Keys.WHOSE_TURN);
    
    // 旧用户停止操作  问题出在以下print的1、2之间
    print("============== 1 ==============="); // print 1
    var oldPlayer = client.Room.GetPlayer(oldActorId);
    await client.SendEvent(Constants.EVENT_CONTROL_END, options: new SendEventOptions
    {
        TargetActorIds = new List<int> { oldPlayer.ActorId }
    });
    print("============== 2 ==============="); // print 2

    // 计算得到下一个回合的actorID
    var nextPlayer = client.Room.PlayerList[0];
    if (oldActorId == nextPlayer.ActorId) nextPlayer = client.Room.PlayerList[1];
    var nextActorId = nextPlayer.ActorId;

    var turn = props.GetInt(Keys.TURN);
    turn += 1;
    var newProps = new PlayObject {
        { Keys.WHOSE_TURN, nextActorId },
        { Keys.TURN, turn },
        { Keys.COUNTDOWN, 4 }
    };
    await client.Room.SetCustomProperties(newProps);

    // 抽卡
    await DrawForPlayer(nextPlayer); // 这里的调用有SetCustomProperties、也有SendEvent
}

问题出在print 1、2之间的那次SendEvent。如果将SendEvent部分(1、2之间所有代码)都注释掉,可以正常运行;如果带有两行print语句及中间的代码,也可以正常运行。但如果只保留SendEvent部分但不带两行Print语句,则会导致卡住,即WaitUntil(() => task.IsCompleted)判断一直是失败的。经过测试,“print 2”行缺少就会导致卡住。实际项目中后面还会有一个sendEvent,经过各种注释掉的测试发现好像sendEvent后不加一个print,就会导致卡住。

今天几乎没干别的就光围绕这个问题去网上做各种收集也去stackoverflow提问,最后发现跟await异步调用无关,做了大量测试,最后确定问题如下。
首先是最小复现模型:

IEnumerator CountingCoroutine()
{
    while (true)
    {
        var client = LeanCloudUtils.GetClient();
        client.SendEvent(123);
        var props = new PlayObject {
            { "32", 45 } // 字符串参数行
            //{ 32, 45 } // 数字参数行
        };
        //print("magic"); // magic行
        client.Room.SetCustomProperties(props);
        yield return new WaitForSeconds(1);
    }
}

另外在接收到自定义事件key为123时做点工作(非print)以检测程序是否正常运行。我做的是初始化一个空GameObject。如果每1秒都初始化一个空GameObject证明正确运行,否则我们称之为卡住。

直接运行以上代码会卡住。
以下方法任何单独之一都可以让程序正确运行:
1 将prop里的key改为字符串,即取消注释“字符串参数行”并注释掉“数字参数行”
2 在sendEvent后加入一个print语句,即注释掉magic行
3 使用该sdk时打开日志(我默认是关闭的),即在程序开始的地方加入以下代码开启日志:

        LeanCloud.Common.Logger.LogDelegate = (level, info) =>
        { Debug.Log($"[{level}] {info}"); };

我花了很多时间精力做了详细的测试,也基本定位了问题所在,希望可以得到尽快解决,谢谢!