Ereshkigal

 view release on metacpan or  search on metacpan

lib/Ereshkigal.pm  view on Meta::CPAN

#                   only ever wording.
#
# Returns nothing meaningful, an empty return.
#
#     # not called directly... armed per child when it is spawned
#     $kernel->sig_child( $wheel->PID, 'kur_reaped' );
sub _poe_kur_reaped {
	my ( $self, $kernel, $pid, $exit ) = @_[ OBJECT, KERNEL, ARG1, ARG2 ];

	my $name = delete( $self->{pid_to_kur}{$pid} );
	if ( !defined($name) ) {
		return;
	}

	my $entry = $self->{kurs}{$name};
	if ( defined($entry) && defined( $entry->{wheel} ) ) {
		delete( $self->{wheel_to_kur}{ $entry->{wheel}->ID } );
		$entry->{wheel} = undef;
		$entry->{pid}   = undef;
	}

	# the raw wait status packs the signal that killed it into the low seven
	# bits and the exit code into the next eight, so decode both rather than
	# shifting blindly... a kur killed by signal 9 and one that exited 9 of
	# its own accord are not the same event and should not read alike
	my $signal = $exit & 127;
	my $how    = $signal ? 'was killed by signal ' . $signal : 'exited with ' . ( $exit >> 8 );
	log_drek( 'info', 'kur "' . $name . '" PID ' . $pid . ' ' . $how );

	if ( $self->{shutting_down} || !defined($entry) || !$entry->{enabled} ) {
		return;
	}

	# it ran long enough to be considered to have started fine, so reset the backoff
	if ( defined( $entry->{spawned} ) && ( time - $entry->{spawned} ) > 60 ) {
		$entry->{delay} = 1;
	}

	my $delay = $entry->{delay};
	$entry->{delay} = $delay * 2 > 60 ? 60 : $delay * 2;
	$entry->{restarts}++;

	# whether a kur comes back is the manager's call and only the manager's...
	# a kur does not get a vote in it by choosing how it exits, so anything
	# the manager did not itself ask for is restarted, tidily as it may have
	# gone. The stops that were asked for never reach here, stop_all having
	# set shutting_down and remove_kur having cleared enabled before either
	# takes a kur down, so what is left is a kur that went away on its own.
	# That is an error whatever status it managed to exit with, and is logged
	# as one... the status only changes the wording, never the decision
	log_drek( 'err', 'kur "' . $name . '" ' . $how . ', restarting in ' . $delay . ' seconds' );

	$kernel->delay_set( 'restart_kur', $delay, $name );

	return;
} ## end sub _poe_kur_reaped

# The manager session's remove_kur handler, which stops one kur and drops it
# from the registry for good. The actual removal has to happen in the manager
# session as destroying a POE::Wheel::Run from within another session leaves
# its watchers behind, keeping the manager session alive forever... which is
# why _cmd_remove_kur only marks the entry disabled and posts here rather
# than doing the work itself.
#
# A running kur is asked to stop over its own socket, so it checkpoints its
# tablets and tears its firewall setup down properly. Only if that fails is
# it sent a TERM, which gets the process gone but leaves whatever it was
# holding in the firewall. Either way the entry is then unwired... its wheel
# dropped from wheel_to_kur, its PID from pid_to_kur, and the registry entry
# deleted.
#
# Those two lookups are cleaned here rather than being left to the reap
# handler because the reap handler only cleans them while the registry entry
# is still around, and it is about to not be... without this every add and
# remove cycle would leak a wheel_to_kur entry.
#
# POE calling convention... invoked as an object state, and note it does not
# take KERNEL...
#
#     $_[OBJECT] :: This Ereshkigal instance.
#     $_[ARG0]   :: The kur instance name to remove, as a plain string. A
#                   name that is no longer registered returns immediately, so
#                   a doubled post is harmless.
#
# Returns nothing meaningful, an empty return.
#
#     # from _cmd_remove_kur, after marking the entry disabled
#     $poe_kernel->post( 'ereshkigal_manager', 'remove_kur', $name );
sub _poe_remove_kur {
	my ( $self, $name ) = @_[ OBJECT, ARG0 ];

	my $entry = $self->{kurs}{$name};
	if ( !defined($entry) ) {
		return;
	}

	if ( defined( $entry->{pid} ) ) {
		eval { $self->_kur_client($name)->call_ok('stop'); };
		if ($@) {
			log_drek( 'err', 'stopping kur "' . $name . '" via it\'s socket failed, sending TERM... ' . $@ );
			if ( defined( $entry->{wheel} ) ) {
				$entry->{wheel}->kill('TERM');
			}
		}
	}

	# the reaped handler only cleans these up while the registry entry is
	# still around, which it is about to not be, so they are cleaned here...
	# otherwise every add/remove cycle leaks a wheel_to_kur entry
	if ( defined( $entry->{wheel} ) ) {
		delete( $self->{wheel_to_kur}{ $entry->{wheel}->ID } );
	}
	if ( defined( $entry->{pid} ) ) {
		delete( $self->{pid_to_kur}{ $entry->{pid} } );
	}

	delete( $self->{kurs}{$name} );

	log_drek( 'info', 'removed kur "' . $name . '"' );

	return;

lib/Ereshkigal.pm  view on Meta::CPAN

#     my $result = $self->_cmd_add_kur(
#         {
#             'args' => {
#                 'name' => 'dns',
#                 'opts' => { 'backend' => 'pf', 'ports' => ['53'] },
#             }
#         }
#     );
#     # { added => 'dns' }
sub _cmd_add_kur {
	my ( $self, $request ) = @_;

	my $args = $request->{args};
	if ( !defined($args) || !defined( $args->{name} ) ) {
		die('args.name must be the name for the new kur instance');
	}
	my $name = $args->{name};

	if ( defined( $self->{kurs}{$name} ) ) {
		die( 'The kur instance "' . $name . '" already exists' );
	}

	$self->_check_kur_def( $name, $args->{opts}, 0 );
	if ( defined( $args->{opts}{fan_out} ) ) {
		$self->_check_fan_out_members( $name, $args->{opts}, 0 );
	}

	$self->{kurs}{$name} = {
		'opts'     => $args->{opts},
		'wheel'    => undef,
		'pid'      => undef,
		'restarts' => 0,
		'delay'    => 1,
		'enabled'  => 1,
		'spawned'  => undef,
	};

	$poe_kernel->post( 'ereshkigal_manager', 'spawn_kur', $name );

	log_drek( 'info', 'added kur "' . $name . '"' );

	return { 'added' => $name };
} ## end sub _cmd_add_kur

# Handles the remove_kur command... refuses while args.name is still a
# fan_out member, otherwise disables it and has the manager session stop and
# drop it.
#
# The gate check is why this is not simply a post. Config load refuses a
# fan_out naming an unknown kur, so removal cannot be allowed to create that
# same dangling state at runtime... every registered kur is scanned for gates
# naming this one, and if any do the removal is refused naming them. Remove
# the gate first, or the member stays.
#
# Setting enabled to 0 before posting matters as well... it is what stops the
# reap handler restarting the kur when the stop it is about to be sent
# actually kills it.
#
# The real work is left to the manager session, as a POE::Wheel::Run must be
# destroyed in the session watching it or its watchers outlive it and keep
# the session alive forever.
#
# The config file is not rewritten, so a kur defined there returns at the
# next manager start.
#
# Note this takes no context and authorizes nothing of its own... the
# dispatch entry has already run _authorize with no kur names.
#
# Args...
#
#     $request :: Required. The decoded request hash ref. Must carry args
#                 holding a name key naming the kur to remove, as a plain
#                 string.
#
# Returns a hash ref of { removed => name } once it is disabled and the
# removal posted. As with add_kur that is a promise the manager will do it,
# not that the process is already gone.
#
# Dies with a plain string when args.name is missing, names no registered
# kur, or is still a member of one or more gates, which names them.
#
#     my $result = $self->_cmd_remove_kur( { 'args' => { 'name' => 'dns' } } );
#     # { removed => 'dns' }
sub _cmd_remove_kur {
	my ( $self, $request ) = @_;

	my $args = $request->{args};
	if ( !defined($args) || !defined( $args->{name} ) ) {
		die('args.name must be the name of a kur instance');
	}
	my $name = $args->{name};

	my $entry = $self->{kurs}{$name};
	if ( !defined($entry) ) {
		die( 'No such kur instance, "' . $name . '"' );
	}

	# config load refuses a fan_out naming an unknown kur, so removal cannot
	# be allowed to create that same dangling state at runtime... remove the
	# gate first, or the member stays
	my @gates_using;
	foreach my $other_name ( sort( keys( %{ $self->{kurs} } ) ) ) {
		my $fan_out = $self->{kurs}{$other_name}{opts}{fan_out};
		if ( defined($fan_out) && grep { $_ eq $name } @{$fan_out} ) {
			push( @gates_using, $other_name );
		}
	}
	if (@gates_using) {
		die(      'The kur "'
				. $name
				. '" is a fan_out member of "'
				. join( '", "', @gates_using )
				. '"... remove the fan_out kur first' );
	}

	$entry->{enabled} = 0;

	# the actual stop and removal happens in the manager session given the
	# wheel has to be destroyed there
	$poe_kernel->post( 'ereshkigal_manager', 'remove_kur', $name );



( run in 1.388 second using v1.01-cache-2.11-cpan-14f38c9f855 )