OpenStack4j是一个OpenStack的Java SDK。
问题描述
在同一个代码处理线程中,首先获取了 projectA 的 OSClient 对象 OSClientA,然后又获取了 projectB 的 OSClient 对象 OSClientB。
后续在用 OSClientA 去调用某个 service(比如 BlockVolumeService)去创建资源(比如 volume)的时候,期望创建在 projectA 下面,结果创建的资源却在 projectB 下面。
查找原因
经过跟踪 OpenStack4j 中获取 OSClient 和 调用具体 service 的相关源码后,发现问题,在于 OSClientSession 类中使用 ThreadLocal 变量 sessions 将获取的 OSClient 存下来。
后续在创建资源的时候,使用从 sessions 中取出的OSClient ,调用 OpenStack 的 API 接口。
关键在于,第二次获取 OSClientB 的时候,会将 sessions 中存的 OSClient 更新,将原先的 OSClientA 给替换为 OSClientB。
也就造成了,尽管是用 OSClientA 去创建资源,但是实际使用的 OSClient 已经被改了,也就是是用 OSClientB 的相关参数去创建的。
源码分析
获取 OSClient 的代码在 OSAuthenticator#authenticateV3(默认使用的是v3版本)。
......
private static OSClientV3 authenticateV3(KeystoneAuth auth, SessionInfo info, Config config) {
if (auth.getType().equals(Type.TOKENLESS)){
......
}
# 调用 OpenStack keystone 的认证接口
HttpRequest request = HttpRequest.builder(KeystoneToken.class)
.header(ClientConstants.HEADER_OS4J_AUTH, TOKEN_INDICATOR).endpoint(info.endpoint)
.method(HttpMethod.POST).path("/auth/tokens").config(config).entity(auth).build();
HttpResponse response = HttpExecutor.create().execute(request);
if (response.getStatus() >= 400) {
try {
throw mapException(response.getStatusMessage(), response.getStatus());
} finally {
HttpEntityHandler.closeQuietly(response);
}
}
KeystoneToken token = response.getEntity(KeystoneToken.class);
token.setId(response.header(ClientConstants.HEADER_X_SUBJECT_TOKEN));
.......
String reqId = response.header(ClientConstants.X_OPENSTACK_REQUEST_ID);
# info.reLinkToExistingSession 在前面的调用过程中传参是 false。
if (!info.reLinkToExistingSession) {
# 创建了一个 OSClient,OSClientSessionV3 是 v3 版本的实现类
OSClientSessionV3 v3 = OSClientSessionV3.createSession(token, info.perspective, info.provider, config);
v3.reqId = reqId;
return v3;
}
OSClientSessionV3 current = (OSClientSessionV3) OSClientSessionV3.getCurrent();
current.token = token;
current.reqId = reqId;
return current;
}
OSClientSessionV3#createSession,也是关键的地方。
public static OSClientSessionV3 createSession(Token token, Facing perspective, CloudProvider provider, Config config) {
return new OSClientSessionV3(token, token.getEndpoint(), perspective, provider, config);
}
......
public static class OSClientSessionV3 extends OSClientSession implements OSClientV3 {
Token token;
protected String reqId;
private OSClientSessionV3(Token token, String endpoint, Facing perspective, CloudProvider provider, Config config) {
this.token = token;
this.config = config;
this.perspective = perspective;
this.provider = provider;
# 重点在这里
sessions.set(this);
}
......
接着看一下 sessions 这个变量
public abstract class OSClientSession> implements EndpointTokenProvider {
private static final Logger LOG = LoggerFactory.getLogger(OSClientSession.class);
# 可以看到 sessions 是一个 ThreadLocal 变量,而每一次创建新的 OSClientSession,会调用 set 方法,
# 覆盖之前的 OSClientSession。
@SuppressWarnings("rawtypes")
private static final ThreadLocal sessions = new ThreadLocal();
Config config;
Facing perspective;
String region;
Set supports;
CloudProvider provider;
Map headers;
EndpointURLResolver fallbackEndpointUrlResolver = new DefaultEndpointURLResolver();
由以上的源码知道,每次获取 OSClient(即创建新的OSClientSessionV3)的时候,会在 sessions 中覆盖之前的。
看完获取的过程,再去确认一下调用具体 service 创建资源的时候,是否是从 sessions 中取出的 OSClient。
无论哪个 service 中方法,最终都是使用统一的 http 调用方法 HttpExecutorServiceImpl#invokeRequest。
......
private HttpResponse invokeRequest(HttpCommand command) throws Exception {
Response response = command.execute();
if (command.getRetries() == 0 && response.getStatus() == 401 && !command.getRequest().getHeaders().containsKey(ClientConstants.HEADER_OS4J_AUTH))
{
# 重点看这个方法的实现,同样是 OSAuthenticator 类中的
OSAuthenticator.reAuthenticate();
command.getRequest().getHeaders().put(ClientConstants.HEADER_X_AUTH_TOKEN, OSClientSession.getCurrent().getTokenId());
return invokeRequest(command.incrementRetriesAndReturn());
}
return HttpResponseImpl.wrap(response);
}
OSAuthenticator.reAuthenticate()
/**
* Re-authenticates/renews the token for the current Session
*/
@SuppressWarnings("rawtypes")
public static void reAuthenticate() {
LOG.debug("Re-Authenticating session due to expired Token or invalid response");
OSClientSession session = OSClientSession.getCurrent();
switch (session.getAuthVersion()) {
case V2:
KeystoneAccess access = ((OSClientSessionV2) session).getAccess().unwrap();
SessionInfo info = new SessionInfo(access.getEndpoint(), session.getPerspective(), true,
session.getProvider());
Auth auth = (Auth) ((access.isCredentialType()) ? access.getCredentials() : access.getTokenAuth());
authenticateV2((org.openstack4j.openstack.identity.v2.domain.Auth) auth, info, session.getConfig());
break;
case V3:
default:
Token token = ((OSClientSessionV3) session).getToken();
info = new SessionInfo(token.getEndpoint(), session.getPerspective(), true, session.getProvider());
# 从 sessions 中获取 OSClientSessionV3 之后,同样调用 authenticateV3 认证
authenticateV3((KeystoneAuth) token.getCredentials(), info, session.getConfig());
break;
}
}
虽然和获取 OSClient 的时候一样,都调用了OSAuthenticator#authenticateV3。但是需要注意,上面说到 info.reLinkToExistingSession 这个参数在获取的时候传参为 false,而这里的传参是 true。
代表它会重新连接已经存在的 Session。
......
private static OSClientV3 authenticateV3(KeystoneAuth auth, SessionInfo info, Config config) {
......
# info.reLinkToExistingSession 在这里传参是 true,所以不会再创建新的。
if (!info.reLinkToExistingSession) {
# 创建了一个 OSClient,OSClientSessionV3 是 v3 版本的实现类
OSClientSessionV3 v3 = OSClientSessionV3.createSession(token, info.perspective, info.provider, config);
v3.reqId = reqId;
return v3;
}
# 取出当前的 OSClient,直接返回。
OSClientSessionV3 current = (OSClientSessionV3) OSClientSessionV3.getCurrent();
current.token = token;
current.reqId = reqId;
return current;
}
结论
从上面的源码分析结合我的问题可以得知,在 OSAuthenticator.reAuthenticate() 取出的当前的 OSClient 是 OSClientB,而不是 OSClientA,所以导致了资源创建在了 projectB 下面。
分享一下自己踩坑的问题分析,希望大家都可以及时发现并避免因此出现意想不到的 Bug。