在Channel.sendChannelOpen调用期间,JSch - wait()无效

时间:2017-01-03 06:16:31

标签: java jsch

我们最近开始在一个客户端网站上发现零星问题,其中JSch开始报告"频道未打开"在成功运行多年后调用sendChannelOpen期间。在深入研究代码并看到两个选项(超时与失败消息)后,我们很可能会遇到失败。

由于消息相同,我下载了源代码并添加了一些输出语句来验证。经过几次测试后,我惊讶地发现我们实际上已经达到了超时部分,但速度很快,以至于在查看代码并考虑wait()语句时没有意义。

为了进一步排除故障,我将一个快速的独立应用程序放在一起,使用以下方法作为核心调用进行测试:

    public static void testConnectionJCraft(String host, String user, String password, int port, int timeout) throws Exception {
    JSch jsch = new JSch();
    Session session = jsch.getSession(user, host, port);
    try {
        session.setPassword(password);
        session.setConfig("StrictHostKeyChecking", "no");
        session.connect();

        if (session.isConnected()) {
            System.out.println("JCraft Connected sucessfully to server : " + host);
        } else {
            throw new Exception("JCraft Connection Failed: " + host);
        }                       
        Channel channel = session.openChannel("sftp");
        try {
            if (timeout > 0) {
                channel.connect(timeout);
            } else {
                channel.connect();
            }
        } finally {
            channel.disconnect();
        }

    } finally {
        if (session != null) {
            session.disconnect();
        }
    }
}

注意 - 我知道if(timeout> 0)是无关紧要的,我可以调用channel.connect(timeout)并在timeout == 0时获得相同的结果,但我想确保调用在我正在排除故障的应用程序中模仿电话,以防万一。

然后我在com.jcraft.jsch.Channel中的sendChannelOpen方法中添加了一些额外的语句:

  protected void sendChannelOpen() throws Exception {
    Session _session=getSession();
    if(!_session.isConnected()){
      throw new JSchException("session is down");
    }

    Packet packet = genChannelOpenPacket();
    _session.write(packet);

    int retry=2000;
    long start=System.currentTimeMillis();
    long timeout=connectTimeout;
    long iteration = 0;
    if(timeout!=0L) retry = 1;
    System.out.println("Timeout: " + timeout);

    long t = timeout== 0L ? 10L : timeout;
    System.out.println("t: " + t);

    synchronized(this){
      while(this.getRecipient()==-1 &&
            _session.isConnected() &&
             retry>0){
        if(timeout>0L){
          long dur = (System.currentTimeMillis()-start);
          if(dur>timeout){
            System.out.println("Dur: " + dur + " > " + timeout);
            retry=0;
            continue;
          }
        }

        try{
          this.notifyme=1;
          wait(t);
        }
        catch(java.lang.InterruptedException e){
            System.out.println("Interrupted?");
        }
        finally{
          this.notifyme=0;
        }
        retry--;
        iteration++;
      }
    }

    long end = System.currentTimeMillis();
    System.out.println("Channel open duration: " + (end - start) + "ms");
    System.out.println("Channel open iterations: " + iteration);
    if(!_session.isConnected()){
      throw new JSchException("session is down");
    }
    if(this.getRecipient()==-1){  // timeout
      throw new JSchException("channel is not opened.");
    }
    if(this.open_confirmation==false){  // SSH_MSG_CHANNEL_OPEN_FAILURE
      throw new JSchException("channel unable to be opened, SSH_MSG_CHANNEL_OPEN_FAILURE received.");
    }
    connected=true;
  }

我确实移动了t的定义,以避免垃圾邮件的价值。报告"频道的尝试结果未打开"消息显示上面的wait(t)语句似乎没有考虑超时。输出示例(服务器名称x' d out):

JCraft Connected sucessfully to server : xxx.xxxx.xxx
Timeout: 0
t: 10
Channel open duration: 8ms
Channel open iterations: 2000
ERROR: channel is not opened.
com.jcraft.jsch.JSchException: channel is not opened.
        at com.jcraft.jsch.Channel.sendChannelOpen(Channel.java:783)
        at com.jcraft.jsch.Channel.connect(Channel.java:153)
        at com.jcraft.jsch.Channel.connect(Channel.java:147)
        at com.gorman.tools.FTPTestUtility.testConnectionJCraft(FTPTestUtility.java:46)
        at com.gorman.tools.FTPTestUtility.main(FTPTestUtility.java:21)

因此,在这种情况下,请求在总共8ms内完成了所有2000次迭代。有时尝试成功并且wait()似乎得到尊重:

JCraft Connected sucessfully to server : xxx.xxxx.xxx
Timeout: 0
t: 10
Notifying all for setRecipient
Channel open duration: 21ms
Channel open iterations: 3

这里我们有3次迭代,当我单独记录它时,我得到了10/11/0的时间分割,这是我所期望的。我确实在调用notifyAll()的区域添加了语句,以确保无意中的调用不会导致等待继续,但是除了设置收件人ID之外没有进行任何调用 - 正如预期的那样。

奇怪的是,如果我将超时值> = 1200ms传递给通话,我就无法实现此目的。我想知道这是否只是wait()的怪癖的副产品,特别是JavaDoc中关于超时值的这个注释:

  • 指定的实时时间已经过去,或多或少

正如我所说,我们已经运行了多年,默认调用了channel.connect(),但我想知道是否有一些新的延迟让我们看到了这个问题。即使对println调用过多也会减慢速度,使得连接速度变慢,看起来像21ms是我现在应该期待的,但在上周五之前一切正常,据我所知,我们还没有看到过这个问题。

感谢任何想法,此时我的解决方法似乎是channel.connect(rationalValue)。我只是想进一步了解这个案子,并把它放在这里,以防它帮助其他人不要拔头发!

1 个答案:

答案 0 :(得分:5)

找到解决方案这似乎是Leap Second的牺牲品,显然需要重启我们的CentOS盒子(正常运行时间为45天)。 RHEL盒子对其他客户来说效果很好,但是这个CentOS盒子正在展示上述行为。

我们已经验证了在12/31之前适当的内核版本,并且ntpdate和tzdata版本高于建议的闰秒版本。对于Dirty Cow补丁,最新的重启是11/18。

然而,在12月18日已经自动应用了许多补丁(补丁级别= BASIC),包括tzdata更新(tzdata-java-2016j-1.el7.noarch)。我们冒了一个机会,走了MS路线,今天重新启动了这个盒子。重启后,wait()调用按预期运行,我们的问题已经清除。