Skip to content

Commit 6c0fc45

Browse files
toddr-botclaude
andcommitted
fix: ReconnectToServer now survives _init_jabber failures
Previously, _init_jabber() called die() on connection/auth failure, and ReconnectToServer didn't wrap the call in eval. This meant any transient server outage during reconnection killed the entire bot instead of retrying with exponential backoff as intended. Now _init_jabber() failures are caught, partial jabber_client state is cleaned up, and the retry loop continues. Also fixes $] version formatting in IQ responses (missing /g flag on leading-zero strip). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2d37863 commit 6c0fc45

File tree

3 files changed

+115
-4
lines changed

3 files changed

+115
-4
lines changed

lib/Net/Jabber/Bot.pm

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,14 @@ sub ReconnectToServer {
580580
INFO("Sleeping $sleep_time before attempting re-connect");
581581
sleep $sleep_time;
582582
$sleep_time *= 2 if ( $sleep_time < 300 );
583-
$self->_init_jabber();
583+
eval { $self->_init_jabber() };
584+
if ($@) {
585+
WARN("Reconnection attempt failed: $@");
586+
# Clean up partial client state so IsConnected() stays false
587+
# and next _init_jabber() creates a fresh client
588+
$self->jabber_client(undef);
589+
next;
590+
}
584591
if ( defined $background_subroutine ) {
585592
INFO("Running background routine.");
586593
&$background_subroutine( $self, 0 ); # call background proc so we can check for errors while down.
@@ -782,7 +789,7 @@ sub _jabber_in_iq_message {
782789
# convert 5.010000 to 5.10.0
783790
my $perl_version = $];
784791
$perl_version =~ s/(\d{3})(?=\d)/$1./g;
785-
$perl_version =~ s/\.0+(\d)/.$1/;
792+
$perl_version =~ s/\.0+(\d)/.$1/g;
786793

787794
$self->jabber_client->VersionSend(
788795
to => $from,

t/07-test_reconnect_failure.t

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!perl
2+
3+
use strict;
4+
use warnings;
5+
6+
# Override sleep to avoid delays in tests
7+
BEGIN { *CORE::GLOBAL::sleep = sub { }; }
8+
9+
use Test::More tests => 10;
10+
use Net::Jabber::Bot;
11+
12+
# stuff for mock client object
13+
use FindBin;
14+
use lib "$FindBin::Bin/lib";
15+
use MockJabberClient; # Test object
16+
17+
my $bot_alias = 'reconnect_fail_bot';
18+
my $server = 'talk.google.com';
19+
20+
my %forums_and_responses;
21+
$forums_and_responses{'test_forum1'} = [ "jbot:", "" ];
22+
23+
my $bot = Net::Jabber::Bot->new(
24+
server => $server,
25+
conference_server => "conference.$server",
26+
port => 5222,
27+
username => 'test_username',
28+
password => 'test_pass',
29+
alias => $bot_alias,
30+
message_function => sub { },
31+
background_function => sub { },
32+
loop_sleep_time => 5,
33+
process_timeout => 5,
34+
forums_and_responses => \%forums_and_responses,
35+
out_messages_per_second => 5,
36+
max_message_size => 800,
37+
max_messages_per_hour => 100,
38+
forum_join_grace => 0,
39+
);
40+
41+
isa_ok( $bot, "Net::Jabber::Bot" );
42+
ok( $bot->IsConnected(), "Bot connected after init" );
43+
44+
# Test 1: _init_jabber dies when Connect returns undef
45+
$bot->Disconnect();
46+
ok( !$bot->IsConnected(), "Bot disconnected" );
47+
48+
$Net::Jabber::Client::connect_fail_remaining = 1;
49+
eval { $bot->_init_jabber() };
50+
like( $@, qr/Jabber server is down/, "_init_jabber dies on connection failure" );
51+
52+
# The partial jabber_client object is left behind — this is the state
53+
# that ReconnectToServer must clean up
54+
ok( defined $bot->jabber_client, "Partial client exists after failed _init_jabber" );
55+
56+
# Clean up for next test
57+
$bot->jabber_client(undef);
58+
59+
# Test 2: ReconnectToServer survives transient failures and reconnects
60+
$Net::Jabber::Client::connect_fail_remaining = 2;
61+
$bot->ReconnectToServer();
62+
ok( $bot->IsConnected(), "ReconnectToServer reconnects after 2 transient failures" );
63+
is( $Net::Jabber::Client::connect_fail_remaining, 0, "All failures consumed" );
64+
65+
# Test 3: Background function runs after successful reconnect
66+
my $bg_called = 0;
67+
my $bot2 = Net::Jabber::Bot->new(
68+
server => $server,
69+
conference_server => "conference.$server",
70+
port => 5222,
71+
username => 'test_username2',
72+
password => 'test_pass',
73+
alias => 'bg_test_bot',
74+
message_function => sub { },
75+
background_function => sub { $bg_called++ },
76+
loop_sleep_time => 5,
77+
process_timeout => 5,
78+
forums_and_responses => \%forums_and_responses,
79+
out_messages_per_second => 5,
80+
max_message_size => 800,
81+
max_messages_per_hour => 100,
82+
forum_join_grace => 0,
83+
);
84+
85+
$bg_called = 0;
86+
$Net::Jabber::Client::connect_fail_remaining = 1;
87+
$bot2->Disconnect();
88+
$bot2->ReconnectToServer();
89+
ok( $bot2->IsConnected(), "Bot2 reconnects after 1 failure" );
90+
ok( $bg_called > 0, "Background function called after successful reconnect" );
91+
92+
# Test 4: Multiple failures with increasing backoff (all caught)
93+
$Net::Jabber::Client::connect_fail_remaining = 5;
94+
$bot->Disconnect();
95+
$bot->ReconnectToServer();
96+
ok( $bot->IsConnected(), "ReconnectToServer survives 5 consecutive failures" );

t/lib/MockJabberClient.pm

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,19 @@ sub SetCallBacks {
6060
$self->{message_callback} = $callbacks{'message'};
6161
}
6262

63+
our $connect_fail_remaining = 0;
64+
6365
sub Connect {
6466
my $self = shift;
65-
67+
6668
$self->{server} = shift;
67-
return 1; # Always confirm we're connected.
69+
70+
if ($connect_fail_remaining > 0) {
71+
$connect_fail_remaining--;
72+
return undef; # Simulate connection failure.
73+
}
74+
75+
return 1; # Confirm we're connected.
6876
}
6977

7078
sub AuthSend {

0 commit comments

Comments
 (0)