upstart で start した job の設定を変更するには一度 stop する必要がある

upstart で start した job の設定を変更した後に反映するには、次のように stop して start しなければいけません。restart ではダメです。restart でダメな理由は後述します。

stop JOB && start JOB

upstart は inotify で設定ファイルのディレクトリを監視しているのでわざわざ initctl reload-configuration を実行しなくてもいいんですが、FAQ を読む限り「読み込むとは言ったが反映するとまでは言ってない」という感じみたいですね…。

Upstart uses inotify to watch the configuration directory for changes, there’s no need to force it to read the job definition again. If a job is running, the changes to the definition will not occur until the job has been stopped; for instance jobs, all instances must be stopped.

cf. http://upstart.ubuntu.com/faq.html

restart JOBstop JOB && start JOB は同義だと思っていたこともあり腑に落ちなかったので、以下調べたことをつらつら。

きっかけ

Presto の worker は state を SHUTTING_DOWN にすると coordinator から task が割り当てられないようになり、最短で shutdown.grace-period (デフォルト 2 分)の 2 倍の時間で停止します。
ところが、Amazone Elastic MapReduce (EMR) だと upstart で Presto server のプロセスが管理されており、 /etc/init/presto-server.confrespawn が指定されているものだから、停止してもすぐに新しいプロセスが起動します。

upstart 1.3 以降であれば、upstart による管理を一時的にやめるには次のようにすれば実現できるらしいです。(この方法も一度 stop しないと反映されないんでしょうが)

echo manual | sudo tee /etc/init/presto-server.override

ところが、現時点の EMR のデフォルト AMI である ami-0550d48e307415901 だと upstart 0.6.5…

[hadoop@ip-172-31-10-216 ~]$ uname -a
Linux ip-172-31-10-216 4.14.72-68.55.amzn1.x86_64 #1 SMP Fri Sep 28 21:14:54 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
[hadoop@ip-172-31-10-216 ~]$ cat /etc/system-release
Amazon Linux AMI release 2018.03
[hadoop@ip-172-31-10-216 ~]$ initctl version
init (upstart 0.6.5)

じゃあ次のように respawn をコメントアウトすれば良いと思ったわけです。inotify で監視しているから勝手に設定も反映されるだろうと。

[hadoop@ip-172-31-10-216 ~]$ diff -u /etc/init/presto-server.conf.orig /etc/init/presto-server.conf
--- /etc/init/presto-server.conf.orig   2018-12-02 07:57:07.235265806 +0000
+++ /etc/init/presto-server.conf        2018-12-02 07:56:58.451396794 +0000
@@ -25,7 +25,7 @@
 stop on stopping netfs
 stop on stopping rsyslog

-respawn
+#respawn

 # respawn unlimited times with 5 seconds time interval
 respawn limit 0 5

ところが反映されない。試行錯誤の結果、冒頭で述べたように stop して start すれば良いという結論にたどり着きました。

sudo stop presto-server && sudo start presto-server

どうして stop する必要があるか?

upstart は inotify で /etc/init 以下を watch することでファイルの変更等があれば勝手に読み込んでくれます。ただ、一度 start してインスタンス化された job は stop するまで使い回されることになります。

「設定ファイルリロードしてくれないんだけど?」というチケットに対しては次のように回答されています。

Actually Upstart reloads such .conf file just fine, what you’ve probably missed is that Upstart will not use the new .conf until all existing instances have stopped.

This is because the new .conf may change such things as locking between the instances, or criterion for instance creation, that would affect any running instances.

cf. Bug #611082 “Upstart does not reload properly .conf files that u…” : Bugs : upstart

設定が変わることで他の job との依存関係とかも変わるかもしれないから、全部 stop するまで反映しないといった感じでしょうか。

動作確認

実際の動作を確認する上で debug ログを有効化しました。次のような変更を加えた上で

--- /etc/rsyslog.conf.orig      2018-12-02 12:21:17.486681338 +0000
+++ /etc/rsyslog.conf   2018-12-02 12:20:54.919018227 +0000
@@ -35,7 +35,7 @@

 # Log anything (except mail) of level info or higher.
 # Don't log private authentication messages!
-*.info;mail.none;authpriv.none;cron.none                /var/log/messages
+*.debug;mail.none;authpriv.none;cron.none                /var/log/messages

 # The authpriv file has restricted access.
 authpriv.*                                              /var/log/secure

rsyslog を再起動し、upstart のログレベルを debug にします。

service rsyslog restart
initctl log-priority debug

これで /etc/init 以下に適当なファイルを作成してみます。

touch /etc/init/foo.conf

そうすると /var/log/messages に次のようなログが出力され、/etc/init/foo.conf をパースすることで新しい JobClass が登録されたことがわかります。

Dec  2 12:26:22 ip-172-31-10-216 init: conf_create_modify_handler: Loading configuration and override files for /etc/init/foo.conf
Dec  2 12:26:22 ip-172-31-10-216 init: conf_create_modify_handler: Loading configuration file /etc/init/foo.conf
Dec  2 12:26:22 ip-172-31-10-216 init: conf_reload_path: Loading foo from /etc/init/foo.conf
Dec  2 12:26:22 ip-172-31-10-216 init: parse_job: Creating new JobClass foo
Dec  2 12:26:22 ip-172-31-10-216 init: job_class_register: Registered job /com/ubuntu/Upstart/jobs/foo

この状態で再度 touch すると

touch /etc/init/foo.conf

次のように既存の JobClass が unregister されて新しい JobClass が登録されたことがわかります。

Dec  2 12:29:07 ip-172-31-10-216 init: conf_create_modify_handler: Loading configuration and override files for /etc/init/foo.conf
Dec  2 12:29:07 ip-172-31-10-216 init: conf_create_modify_handler: Loading configuration file /etc/init/foo.conf
Dec  2 12:29:07 ip-172-31-10-216 init: job_class_unregister: Unregistered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:29:07 ip-172-31-10-216 init: conf_file_destroy: Destroyed unused job foo
Dec  2 12:29:07 ip-172-31-10-216 init: conf_reload_path: Loading foo from /etc/init/foo.conf
Dec  2 12:29:07 ip-172-31-10-216 init: parse_job: Creating new JobClass foo
Dec  2 12:29:07 ip-172-31-10-216 init: job_class_register: Registered job /com/ubuntu/Upstart/jobs/foo

ここで、job を起動してみます。

start foo

job_class_register のログが大量に出るので foo に関係するものだけ抜粋してみます。ここで初めて JobClass のインスタンスである Job が登録されていることがわかります。

Dec  2 12:32:23 ip-172-31-10-216 init: job_class_register: Registered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:32:23 ip-172-31-10-216 init: job_register: Registered instance /com/ubuntu/Upstart/jobs/foo/_
Dec  2 12:32:23 ip-172-31-10-216 init: job_register: Registered instance /com/ubuntu/Upstart/jobs/foo/_
Dec  2 12:32:23 ip-172-31-10-216 init: foo goal changed from stop to start
Dec  2 12:32:23 ip-172-31-10-216 init: foo state changed from waiting to starting
Dec  2 12:32:23 ip-172-31-10-216 init: event_new: Pending starting event
Dec  2 12:32:23 ip-172-31-10-216 init: Handling starting event
Dec  2 12:32:23 ip-172-31-10-216 init: event_finished: Finished starting event
Dec  2 12:32:23 ip-172-31-10-216 init: foo state changed from starting to pre-start
Dec  2 12:32:23 ip-172-31-10-216 init: foo state changed from pre-start to spawned
Dec  2 12:32:23 ip-172-31-10-216 init: foo state changed from spawned to post-start
Dec  2 12:32:23 ip-172-31-10-216 init: foo state changed from post-start to running
Dec  2 12:32:23 ip-172-31-10-216 init: event_new: Pending started event
Dec  2 12:32:23 ip-172-31-10-216 init: Handling started event
Dec  2 12:32:23 ip-172-31-10-216 init: event_finished: Finished started event

この状態で再度 touch します。

touch /etc/init/foo.conf

そうすると JobClass が作成された文言は出ますが、register 云々といったログは出てきません。

Dec  2 12:34:17 ip-172-31-10-216 init: conf_create_modify_handler: Loading configuration and override files for /etc/init/foo.conf
Dec  2 12:34:17 ip-172-31-10-216 init: conf_create_modify_handler: Loading configuration file /etc/init/foo.conf
Dec  2 12:34:17 ip-172-31-10-216 init: conf_reload_path: Loading foo from /etc/init/foo.conf
Dec  2 12:34:17 ip-172-31-10-216 init: parse_job: Creating new JobClass foo

この状態で restart してみます。

restart foo

次のように start の時と似たようなログが出てきました。

Dec  2 12:37:42 ip-172-31-10-216 init: job_class_register: Registered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:37:42 ip-172-31-10-216 init: job_register: Registered instance /com/ubuntu/Upstart/jobs/foo/_
Dec  2 12:37:42 ip-172-31-10-216 init: foo goal changed from start to stop
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from running to stopping
Dec  2 12:37:42 ip-172-31-10-216 init: event_new: Pending stopping event
Dec  2 12:37:42 ip-172-31-10-216 init: foo goal changed from stop to start
Dec  2 12:37:42 ip-172-31-10-216 init: Handling stopping event
Dec  2 12:37:42 ip-172-31-10-216 init: event_finished: Finished stopping event
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from stopping to killed
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from killed to post-stop
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from post-stop to starting
Dec  2 12:37:42 ip-172-31-10-216 init: event_new: Pending starting event
Dec  2 12:37:42 ip-172-31-10-216 init: Handling starting event
Dec  2 12:37:42 ip-172-31-10-216 init: event_finished: Finished starting event
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from starting to pre-start
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from pre-start to spawned
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from spawned to post-start
Dec  2 12:37:42 ip-172-31-10-216 init: foo state changed from post-start to running
Dec  2 12:37:42 ip-172-31-10-216 init: event_new: Pending started event
Dec  2 12:37:42 ip-172-31-10-216 init: Handling started event
Dec  2 12:37:42 ip-172-31-10-216 init: event_finished: Finished started event

次は stop してみます。

stop foo

restart の時には出てこなかったログが出ているので、restart JOBstop JOB && start JOB は全然別物だということがわかると思います。

Dec  2 12:39:24 ip-172-31-10-216 init: job_class_register: Registered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:39:24 ip-172-31-10-216 init: job_register: Registered instance /com/ubuntu/Upstart/jobs/foo/_
Dec  2 12:39:24 ip-172-31-10-216 init: foo goal changed from start to stop
Dec  2 12:39:24 ip-172-31-10-216 init: foo state changed from running to stopping
Dec  2 12:39:24 ip-172-31-10-216 init: event_new: Pending stopping event
Dec  2 12:39:24 ip-172-31-10-216 init: Handling stopping event
Dec  2 12:39:24 ip-172-31-10-216 init: event_finished: Finished stopping event
Dec  2 12:39:24 ip-172-31-10-216 init: foo state changed from stopping to killed
Dec  2 12:39:24 ip-172-31-10-216 init: foo state changed from killed to post-stop
Dec  2 12:39:24 ip-172-31-10-216 init: foo state changed from post-stop to waiting
Dec  2 12:39:24 ip-172-31-10-216 init: event_new: Pending stopped event
Dec  2 12:39:24 ip-172-31-10-216 init: job_class_unregister: Unregistered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:39:24 ip-172-31-10-216 init: job_class_unregister: Unregistered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:39:24 ip-172-31-10-216 init: job_class_register: Registered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:39:24 ip-172-31-10-216 init: job_class_register: Registered job /com/ubuntu/Upstart/jobs/foo
Dec  2 12:39:24 ip-172-31-10-216 init: job_change_state: Destroyed unused job foo
Dec  2 12:39:24 ip-172-31-10-216 init: Handling stopped event
Dec  2 12:39:24 ip-172-31-10-216 init: event_finished: Finished stopped event

ソースコードリーディング

upstart 0.6.5 のソースコードを眺めてみました。

curl -O http://upstart.ubuntu.com/download/0.6/upstart-0.6.5.tar.gz

ファイルが追加されたり更新される度に新しい JobClass が作成されますが、start すると最初に作成されて登録された JobClassinstancesJob が追加されます。これが stop の際に削除されるまでずっと使い続けられます。

/**
 * job_new:
 * @class: class of job,
 * @name: name for new instance.
 *
 * Allocates and returns a new Job structure for the @class given,
 * appending it to the list of instances for @class.  The returned job
 * will also be an nih_alloc() child of @class.
 *
 * @name is used to uniquely identify the instance and is normally
 * generated by expanding the @class's instance member.
 *
 * Returns: newly allocated job structure or NULL if insufficient memory.
 **/
Job *
job_new (JobClass   *class,
	 const char *name)
{
	Job *job;
	int  i;

	nih_assert (class != NULL);
	nih_assert (name != NULL);

	control_init ();

	job = nih_new (class, Job);
	if (! job)
		return NULL;

	(snip)

	nih_hash_add (class->instances, &job->entry);

	NIH_LIST_FOREACH (control_conns, iter) {
		NihListEntry   *entry = (NihListEntry *)iter;
		DBusConnection *conn = (DBusConnection *)entry->data;

		job_register (job, conn, TRUE);
	}

	return job;

error:
	nih_free (job);
	return NULL;
}

というのも、追加された Job が削除されるのは state が waiting になった時のみで、waiting になるのが stop した時のみだからです。

/**
 * job_change_state:
 * @job: job to change state of,
 * @state: state to change to.
 *
 * This function changes the current state of a @job to the new @state
 * given, performing any actions to correctly enter the new state (such
 * as spawning scripts or processes).
 *
 * The associated event is also queued by this function.
 *
 * Some state transitions are not be permitted and will result in an
 * assertion failure.  Also some state transitions may result in further
 * transitions, so the state when this function returns may not be the
 * state requested.
 *
 * WARNING: On return from this function, @job may no longer be valid
 * since it will be freed once it becomes fully stopped.
 **/
void
job_change_state (Job      *job,
		  JobState  state)
{
	nih_assert (job != NULL);

	while (job->state != state) {
		JobState old_state;
		int      unused;

		nih_assert (job->blocker == NULL);

		nih_info (_("%s state changed from %s to %s"), job_name (job),
			  job_state_name (job->state), job_state_name (state));

		old_state = job->state;
		job->state = state;

		/* Perform whatever action is necessary to enter the new
		 * state, such as executing a process or emitting an event.
		 */
		switch (job->state) {

		(snip)

		case JOB_WAITING:
			nih_assert (job->goal == JOB_STOP);
			nih_assert ((old_state == JOB_POST_STOP)
				    || (old_state == JOB_STARTING));

			job_emit_event (job);

			job_finished (job, FALSE);

			/* Remove the job from the list of instances and
			 * then allow a better class to replace us
			 * in the hash table if we have no other instances
			 * and there is one.
			 */
			nih_list_remove (&job->entry);
			unused = job_class_reconsider (job->class);

			(snip)
		}
	}
}

ログに表示されていたように、restart の場合、次のように state が変化します。

  1. running
  2. pre-stop
  3. stopping
  4. killed
  5. post-stop
  6. starting
  7. pre-start
  8. spawend
  9. post-start
  10. running

一方、stop の場合は次のように変化します。

  1. running
  2. pre-stop
  3. stopping
  4. killed
  5. post-stop
  6. waiting

post-stop 状態からどの状態に遷移するかは job_next_state 関数に記述されています。

/**
 * job_next_state:
 * @job: job undergoing state change.
 *
 * The next state a job needs to change into is not always obvious as it
 * depends both on the current state and the ultimate goal of the job, ie.
 * whether we're moving towards stop or start.
 *
 * This function contains the logic to decide the next state the job should
 * be in based on the current state and goal.
 *
 * It is up to the caller to ensure the goal is set appropriately before
 * calling this function, for example setting it to JOB_STOP if something
 * failed.  It is also up to the caller to actually set the new state as
 * this simply returns the suggested one.
 *
 * Returns: suggested state to change to.
 **/
JobState
job_next_state (Job *job)
{
	nih_assert (job != NULL);

	switch (job->state) {

	(snip)

	case JOB_POST_STOP:
		switch (job->goal) {
		case JOB_STOP:
			return JOB_WAITING;
		case JOB_START:
			return JOB_STARTING;
		default:
			nih_assert_not_reached ();
		}
	default:
		nih_assert_not_reached ();
	}
}

restart の場合、goal は start、stop の場合は stop なので、waiting になるのは stop の時だけになるわけです。

以上のことから、起動中の job の設定変更を反映させたい場合は必ず stop させなければいけないと言えます。