Skip to content

Commit 1a20aa9

Browse files
committed
mainloop: propagate errors and implement connection retry
Fixes #1336 by returning the proper exit code and retrying connections
1 parent fa9de10 commit 1a20aa9

File tree

5 files changed

+132
-66
lines changed

5 files changed

+132
-66
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
### Fixed
1010
- revert default features to `alsa_backend` ([#1337])
1111
- do not require `bindgen` dependencies on supported systems ([#1340])
12+
- always return exitcode 1 on error ([#1338])
1213

1314
[#1337]: https://github.com/Spotifyd/spotifyd/pull/1337
15+
[#1338]: https://github.com/Spotifyd/spotifyd/pull/1338
1416
[#1340]: https://github.com/Spotifyd/spotifyd/pull/1340
1517

1618
## [0.4.0]

src/main.rs

+2-5
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ use config::ExecutionMode;
77
#[cfg(unix)]
88
use daemonize::Daemonize;
99
use fern::colors::ColoredLevelConfig;
10-
#[cfg(unix)]
11-
use log::error;
1210
use log::{info, trace, LevelFilter};
1311
use oauth::run_oauth;
1412
#[cfg(target_os = "openbsd")]
@@ -172,7 +170,7 @@ fn run_daemon(mut cli_config: CliConfig) -> eyre::Result<()> {
172170
}
173171
match daemonize.start() {
174172
Ok(_) => info!("Detached from shell, now running in background."),
175-
Err(e) => error!("Something went wrong while daemonizing: {}", e),
173+
Err(e) => return Err(e).wrap_err("something went wrong while daemonizing"),
176174
};
177175
}
178176
#[cfg(target_os = "windows")]
@@ -228,7 +226,6 @@ fn run_daemon(mut cli_config: CliConfig) -> eyre::Result<()> {
228226
let runtime = Runtime::new().unwrap();
229227
runtime.block_on(async {
230228
let initial_state = setup::initial_state(internal_config)?;
231-
initial_state.run().await;
232-
Ok(())
229+
initial_state.run().await
233230
})
234231
}

src/main_loop.rs

+72-51
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use crate::config::{DBusType, MprisConfig};
33
#[cfg(feature = "dbus_mpris")]
44
use crate::dbus_mpris::DbusServer;
55
use crate::process::spawn_program_on_event;
6+
use crate::utils::Backoff;
7+
use color_eyre::eyre::{self, Context};
68
use futures::future::Either;
79
#[cfg(not(feature = "dbus_mpris"))]
810
use futures::future::Pending;
@@ -24,7 +26,7 @@ use librespot_playback::{
2426
mixer::Mixer,
2527
player::Player,
2628
};
27-
use log::error;
29+
use log::{error, info};
2830
use std::pin::Pin;
2931
use std::sync::Arc;
3032

@@ -102,43 +104,63 @@ impl MainLoop {
102104
async fn get_connection(&mut self) -> Result<ConnectionInfo<impl Future<Output = ()>>, Error> {
103105
let creds = self.credentials_provider.get_credentials().await;
104106

105-
let session = Session::new(self.session_config.clone(), self.cache.clone());
106-
let player = {
107-
let audio_device = self.audio_device.clone();
108-
let audio_format = self.audio_format;
109-
let backend = self.backend;
110-
Player::new(
111-
self.player_config.clone(),
107+
let mut connection_backoff = Backoff::default();
108+
loop {
109+
let session = Session::new(self.session_config.clone(), self.cache.clone());
110+
let player = {
111+
let audio_device = self.audio_device.clone();
112+
let audio_format = self.audio_format;
113+
let backend = self.backend;
114+
Player::new(
115+
self.player_config.clone(),
116+
session.clone(),
117+
self.mixer.get_soft_volume(),
118+
move || backend(audio_device, audio_format),
119+
)
120+
};
121+
122+
// TODO: expose is_group
123+
match Spirc::new(
124+
ConnectConfig {
125+
name: self.device_name.clone(),
126+
device_type: self.device_type,
127+
is_group: false,
128+
initial_volume: self.initial_volume,
129+
has_volume_ctrl: self.has_volume_ctrl,
130+
},
112131
session.clone(),
113-
self.mixer.get_soft_volume(),
114-
move || backend(audio_device, audio_format),
132+
creds.clone(),
133+
player.clone(),
134+
self.mixer.clone(),
115135
)
116-
};
117-
118-
// TODO: expose is_group
119-
Spirc::new(
120-
ConnectConfig {
121-
name: self.device_name.clone(),
122-
device_type: self.device_type,
123-
is_group: false,
124-
initial_volume: self.initial_volume,
125-
has_volume_ctrl: self.has_volume_ctrl,
126-
},
127-
session.clone(),
128-
creds,
129-
player.clone(),
130-
self.mixer.clone(),
131-
)
132-
.await
133-
.map(|(spirc, spirc_task)| ConnectionInfo {
134-
spirc,
135-
session,
136-
player,
137-
spirc_task,
138-
})
136+
.await
137+
{
138+
Ok((spirc, spirc_task)) => {
139+
break Ok(ConnectionInfo {
140+
spirc,
141+
session,
142+
player,
143+
spirc_task,
144+
})
145+
}
146+
Err(err) => {
147+
let Ok(backoff) = connection_backoff.next_backoff() else {
148+
break Err(err);
149+
};
150+
error!("connection to spotify failed: {err}");
151+
info!(
152+
"retrying connection in {} seconds (retry {}/{})",
153+
backoff.as_secs(),
154+
connection_backoff.retries(),
155+
connection_backoff.max_retries()
156+
);
157+
tokio::time::sleep(backoff).await;
158+
}
159+
}
160+
}
139161
}
140162

141-
pub(crate) async fn run(mut self) {
163+
pub(crate) async fn run(mut self) -> eyre::Result<()> {
142164
tokio::pin! {
143165
let ctrl_c = tokio::signal::ctrl_c();
144166
// we don't necessarily have a dbus server
@@ -157,18 +179,15 @@ impl MainLoop {
157179
None
158180
};
159181

160-
'mainloop: loop {
182+
let mainloop_result: eyre::Result<()> = 'mainloop: loop {
161183
let connection = tokio::select!(
162184
_ = &mut ctrl_c => {
163-
break 'mainloop;
185+
break 'mainloop Ok(());
164186
}
165187
connection = self.get_connection() => {
166188
match connection {
167189
Ok(connection) => connection,
168-
Err(err) => {
169-
error!("failed to connect to spotify: {}", err);
170-
break 'mainloop;
171-
}
190+
Err(err) => break 'mainloop Err(err).wrap_err("failed to connect to spotify"),
172191
}
173192
}
174193
);
@@ -184,9 +203,8 @@ impl MainLoop {
184203
.as_mut()
185204
.set_session(shared_spirc.clone(), connection.session)
186205
{
187-
error!("failed to configure dbus server: {err}");
188206
let _ = shared_spirc.shutdown();
189-
break 'mainloop;
207+
break 'mainloop Err(err).wrap_err("failed to configure dbus server");
190208
}
191209
}
192210

@@ -204,7 +222,7 @@ impl MainLoop {
204222
// the program should shut down
205223
_ = &mut ctrl_c => {
206224
let _ = shared_spirc.shutdown();
207-
break 'mainloop;
225+
break 'mainloop Ok(());
208226
}
209227
// spirc was shut down by some external factor
210228
_ = &mut spirc_task => {
@@ -214,12 +232,9 @@ impl MainLoop {
214232
result = &mut dbus_server => {
215233
#[cfg(feature = "dbus_mpris")]
216234
{
217-
if let Err(err) = result {
218-
error!("DBus terminated unexpectedly: {err}");
219-
}
220235
let _ = shared_spirc.shutdown();
221236
*dbus_server.as_mut() = Either::Right(future::pending());
222-
break 'mainloop;
237+
break 'mainloop result.wrap_err("DBus terminated unexpectedly");
223238
}
224239
#[cfg(not(feature = "dbus_mpris"))]
225240
result // unused variable
@@ -252,21 +267,27 @@ impl MainLoop {
252267
#[cfg(feature = "dbus_mpris")]
253268
if let Either::Left(dbus_server) = Either::as_pin_mut(dbus_server.as_mut()) {
254269
if let Err(err) = dbus_server.drop_session() {
255-
error!("failed to reconfigure dbus server: {err}");
256-
break 'mainloop;
270+
break 'mainloop Err(err).wrap_err("failed to reconfigure DBus server");
257271
}
258272
}
259-
}
273+
};
274+
260275
if let CredentialsProvider::Discovery { stream, .. } = self.credentials_provider {
261276
let _ = stream.into_inner().shutdown().await;
262277
}
263278
#[cfg(feature = "dbus_mpris")]
264279
if let Either::Left(dbus_server) = Either::as_pin_mut(dbus_server.as_mut()) {
265280
if dbus_server.shutdown() {
266281
if let Err(err) = dbus_server.await {
267-
error!("failed to shutdown the dbus server: {err}");
282+
let err = Err(err).wrap_err("failed to shutdown DBus server");
283+
if mainloop_result.is_ok() {
284+
return err;
285+
} else {
286+
error!("additional error while shutting down: {err:?}");
287+
}
268288
}
269289
}
270290
}
291+
mainloop_result
271292
}
272293
}

src/setup.rs

+10-9
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::alsa_mixer;
33
use crate::{
44
config,
55
main_loop::{self, CredentialsProvider},
6+
utils::Backoff,
67
};
78
use color_eyre::{eyre::eyre, Section};
89
use futures::StreamExt as _;
@@ -11,7 +12,7 @@ use librespot_playback::{
1112
mixer::{self, Mixer, MixerConfig},
1213
};
1314
use log::{debug, error, info};
14-
use std::{sync::Arc, thread, time::Duration};
15+
use std::{sync::Arc, thread};
1516

1617
pub(crate) fn initial_state(
1718
config: config::SpotifydConfig,
@@ -75,9 +76,7 @@ pub(crate) fn initial_state(
7576
let discovery = if config.discovery {
7677
info!("Starting zeroconf server to advertise on local network.");
7778
debug!("Using device id '{}'", session_config.device_id);
78-
const RETRY_MAX: u8 = 4;
79-
let mut retry_counter = 0;
80-
let mut backoff = Duration::from_secs(5);
79+
let mut retry_backoff = Backoff::default();
8180
loop {
8281
match librespot_discovery::Discovery::builder(
8382
session_config.device_id.clone(),
@@ -91,15 +90,17 @@ pub(crate) fn initial_state(
9190
Ok(discovery_stream) => break Some(discovery_stream),
9291
Err(err) => {
9392
error!("failed to enable discovery: {err}");
94-
if retry_counter >= RETRY_MAX {
93+
let Ok(backoff) = retry_backoff.next_backoff() else {
9594
error!("maximum amount of retries exceeded");
9695
break None;
97-
}
96+
};
9897
info!("retrying discovery in {} seconds", backoff.as_secs());
9998
thread::sleep(backoff);
100-
retry_counter += 1;
101-
backoff *= 2;
102-
info!("trying to enable discovery (retry {retry_counter}/{RETRY_MAX})");
99+
info!(
100+
"trying to enable discovery (retry {}/{})",
101+
retry_backoff.retries(),
102+
retry_backoff.max_retries()
103+
);
103104
}
104105
}
105106
}

src/utils.rs

+46-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use log::trace;
2-
use std::env;
2+
use std::{env, time::Duration};
33

44
#[cfg(any(
55
target_os = "freebsd",
@@ -82,6 +82,51 @@ pub(crate) fn get_shell() -> Option<String> {
8282
shell
8383
}
8484

85+
/// Implements basic exponential backoff.
86+
/// The used base is 2.
87+
pub(crate) struct Backoff {
88+
retry_counter: u8,
89+
max_retries: u8,
90+
initial_backoff: Duration,
91+
}
92+
93+
impl Backoff {
94+
pub(crate) fn new(max_retries: u8, initial_backoff: Duration) -> Self {
95+
Self {
96+
retry_counter: 0,
97+
max_retries,
98+
initial_backoff,
99+
}
100+
}
101+
102+
/// Get the next backoff duration.
103+
///
104+
/// Increases the retry counter and returns the next required backoff duration,
105+
/// if available.
106+
pub(crate) fn next_backoff(&mut self) -> Result<Duration, ()> {
107+
if self.retry_counter >= self.max_retries {
108+
return Err(());
109+
}
110+
let backoff_duration = self.initial_backoff * 2u32.pow(self.retry_counter as u32);
111+
self.retry_counter += 1;
112+
Ok(backoff_duration)
113+
}
114+
115+
pub(crate) fn retries(&self) -> u8 {
116+
self.retry_counter
117+
}
118+
119+
pub(crate) fn max_retries(&self) -> u8 {
120+
self.max_retries
121+
}
122+
}
123+
124+
impl Default for Backoff {
125+
fn default() -> Self {
126+
Self::new(4, Duration::from_secs(5))
127+
}
128+
}
129+
85130
#[cfg(test)]
86131
mod tests {
87132
use super::*;

0 commit comments

Comments
 (0)