How to avoid re-register to Eureka server when a Eureka client detects config changes from Config Server? - netflix-eureka

I have set up a Spring Cloud Environment, including an Eureka Server, a Config Server using git as data source, an Eureka client using the Config Server and polling config periodically. I have seen each time I change config through git, the client will first deregister itself from Eureka Server and then register again. How can I avoid the deregister-register process?
In the client, I used #EnableScheduling to enable Spring Scheduling, and created a class ConfigGitClientWatch to poll Config server, another class MyContextRefresher to apply polled config changes to Spring Environment.
ConfigGitClientWatch:
#Component
#Slf4j
public class ConfigGitClientWatch implements Closeable, EnvironmentAware {
private final AtomicBoolean running = new AtomicBoolean(false);
private final AtomicReference<String> version = new AtomicReference<>();
private final MyContextRefresher refresher;
private final ConfigServicePropertySourceLocator locator;
private Environment environment;
public ConfigGitClientWatch(
MyContextRefresher refresher, ConfigServicePropertySourceLocator locator) {
this.refresher = refresher;
this.locator = locator;
}
#Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
#PostConstruct
public void start() {
running.compareAndSet(false, true);
}
#Scheduled(
initialDelayString = "${spring.cloud.config.watch.git.initialDelay:180000}",
fixedDelayString = "${spring.cloud.config.watch.git.delay:500}"
)
public void watchConfigServer() {
if (running.get()) {
String newVersion = fetchNewVersion();
String oldVersion = version.get();
if (versionChanged(oldVersion, newVersion)) {
version.set(newVersion);
final Set<String> refreshedProperties = refresher.refresh();
if(!refreshedProperties.isEmpty()) {
log.info("Refreshed properties:{}", String.join(",", refreshedProperties));
}
}
}
}
private String fetchNewVersion() {
CompositePropertySource propertySource = (CompositePropertySource) locator.locate(environment);
return (String) propertySource.getProperty("config.client.version");
}
private static boolean versionChanged(String oldVersion, String newVersion) {
return !hasText(oldVersion) && hasText(newVersion)
|| hasText(oldVersion) && !oldVersion.equals(newVersion);
}
#Override
public void close() {
running.compareAndSet(true, false);
}
}
MyContextRefresher:
#Component
#Slf4j
public class MyContextRefresher {
private static final String REFRESH_ARGS_PROPERTY_SOURCE = "refreshArgs";
private static final String[] DEFAULT_PROPERTY_SOURCES = new String[] {
// order matters, if cli args aren't first, things get messy
CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME,
"defaultProperties" };
private Set<String> standardSources = new HashSet<>(
Arrays.asList(StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME,
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.JNDI_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME,
StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME,
"configurationProperties"));
private ConfigurableApplicationContext context;
private RefreshScope scope;
public MyContextRefresher(ConfigurableApplicationContext context, RefreshScope scope) {
this.context = context;
this.scope = scope;
}
protected ConfigurableApplicationContext getContext() {
return this.context;
}
protected RefreshScope getScope() {
return this.scope;
}
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
if(!keys.isEmpty()) {
this.scope.refreshAll();
}
return keys;
}
private final List<String> skippedKeys = Arrays.asList(
"config.client.version",
"spring.cloud.client.hostname",
"local.server.port"
);
public synchronized Set<String> refreshEnvironment() {
return addConfigFilesToEnvironment();
}
private Set<String> addConfigFilesToEnvironment() {
Map<String, Object> before = extract(
this.context.getEnvironment().getPropertySources());
ConfigurableApplicationContext capture = null;
Set<String> changedKeys = new HashSet<>();
try {
StandardEnvironment environment = copyEnvironment(
this.context.getEnvironment());
SpringApplicationBuilder builder = new SpringApplicationBuilder(Empty.class)
.bannerMode(Mode.OFF).web(WebApplicationType.NONE)
.environment(environment);
// Just the listeners that affect the environment (e.g. excluding logging
// listener because it has side effects)
builder.application()
.setListeners(Arrays.asList(new BootstrapApplicationListener(),
new ConfigFileApplicationListener()));
capture = builder.run();
if (environment.getPropertySources().contains(REFRESH_ARGS_PROPERTY_SOURCE)) {
environment.getPropertySources().remove(REFRESH_ARGS_PROPERTY_SOURCE);
}
MutablePropertySources target = this.context.getEnvironment()
.getPropertySources();
String targetName = null;
for (PropertySource<?> source : environment.getPropertySources()) {
String name = source.getName();
if (target.contains(name)) {
targetName = name;
}
if (!this.standardSources.contains(name)) {
if (target.contains(name)) {
target.replace(name, source);
}
else {
if (targetName != null) {
target.addAfter(targetName, source);
}
else {
// targetName was null so we are at the start of the list
target.addFirst(source);
targetName = name;
}
}
}
}
final Map<String, Object> after = extract(environment.getPropertySources());
changedKeys = changes(before, after).keySet();
changedKeys.removeAll(skippedKeys);
}
finally {
if(!changedKeys.isEmpty()) {
ConfigurableApplicationContext closeable = capture;
while (closeable != null) {
try {
closeable.close();
} catch (Exception e) {
// Ignore;
}
if (closeable.getParent() instanceof ConfigurableApplicationContext) {
closeable = (ConfigurableApplicationContext) closeable.getParent();
} else {
break;
}
}
this.context.publishEvent(new EnvironmentChangeEvent(this.context, changedKeys));
}
}
return changedKeys;
}
// Don't use ConfigurableEnvironment.merge() in case there are clashes with property
// source names
private StandardEnvironment copyEnvironment(ConfigurableEnvironment input) {
StandardEnvironment environment = new StandardEnvironment();
MutablePropertySources capturedPropertySources = environment.getPropertySources();
// Only copy the default property source(s) and the profiles over from the main
// environment (everything else should be pristine, just like it was on startup).
for (String name : DEFAULT_PROPERTY_SOURCES) {
if (input.getPropertySources().contains(name)) {
if (capturedPropertySources.contains(name)) {
capturedPropertySources.replace(name,
input.getPropertySources().get(name));
}
else {
capturedPropertySources.addLast(input.getPropertySources().get(name));
}
}
}
environment.setActiveProfiles(input.getActiveProfiles());
environment.setDefaultProfiles(input.getDefaultProfiles());
Map<String, Object> map = new HashMap<String, Object>();
map.put("spring.jmx.enabled", false);
map.put("spring.main.sources", "");
capturedPropertySources
.addFirst(new MapPropertySource(REFRESH_ARGS_PROPERTY_SOURCE, map));
return environment;
}
private Map<String, Object> changes(Map<String, Object> before,
Map<String, Object> after) {
Map<String, Object> result = new HashMap<String, Object>();
for (String key : before.keySet()) {
if (!after.containsKey(key)) {
result.put(key, null);
}
else if (!equal(before.get(key), after.get(key))) {
result.put(key, after.get(key));
}
}
for (String key : after.keySet()) {
if (!before.containsKey(key)) {
result.put(key, after.get(key));
}
}
return result;
}
private boolean equal(Object one, Object two) {
if (one == null && two == null) {
return true;
}
if (one == null || two == null) {
return false;
}
return one.equals(two);
}
private Map<String, Object> extract(MutablePropertySources propertySources) {
Map<String, Object> result = new HashMap<String, Object>();
List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
for (PropertySource<?> source : propertySources) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
if (!this.standardSources.contains(source.getName())) {
extract(source, result);
}
}
return result;
}
private void extract(PropertySource<?> parent, Map<String, Object> result) {
if (parent instanceof CompositePropertySource) {
try {
List<PropertySource<?>> sources = new ArrayList<PropertySource<?>>();
for (PropertySource<?> source : ((CompositePropertySource) parent)
.getPropertySources()) {
sources.add(0, source);
}
for (PropertySource<?> source : sources) {
extract(source, result);
}
}
catch (Exception e) {
return;
}
}
else if (parent instanceof EnumerablePropertySource) {
for (String key : ((EnumerablePropertySource<?>) parent).getPropertyNames()) {
result.put(key, parent.getProperty(key));
}
}
}
#Configuration
protected static class Empty {
}
}
The log of the client is as below:
c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://172.39.8.118:14102/
c.c.c.ConfigServicePropertySourceLocator : Located environment: name=user-service, profiles=[peer2], label=null, version=3961593acd49e60c194aebc224adc6a4dfa9f530, state=null
trationDelegate$BeanPostProcessorChecker : Bean 'org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration' of type [org.springframework.cloud.autoconfigure.ConfigurationPropertiesRebinderAutoConfiguration$$EnhancerBySpringCGLIB$$39b14aa7] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING
com.netflix.discovery.DiscoveryClient : Initializing Eureka in region us-east-1
c.n.d.provider.DiscoveryJerseyProvider : Using JSON encoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using JSON decoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using XML encoding codec XStreamXml
c.n.d.provider.DiscoveryJerseyProvider : Using XML decoding codec XStreamXml
c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
com.netflix.discovery.DiscoveryClient : Disable delta property : false
com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null
com.netflix.discovery.DiscoveryClient : Force full registry fetch : false
com.netflix.discovery.DiscoveryClient : Application is null : false
com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true
com.netflix.discovery.DiscoveryClient : Application version is -1: true
com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server
com.netflix.discovery.DiscoveryClient : The response status is 200
com.netflix.discovery.DiscoveryClient : Not registering with Eureka server per configuration
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1570780512926 with initial instances count: 6
o.s.c.n.e.s.EurekaServiceRegistry : Registering application USER-SERVICE with eureka with status UP
c.c.c.ConfigServicePropertySourceLocator : Fetching config from server at : http://172.39.8.118:14102/
c.c.c.ConfigServicePropertySourceLocator : Located environment: name=user-service, profiles=[peer2], label=null, version=3961593acd49e60c194aebc224adc6a4dfa9f530, state=null
b.c.PropertySourceBootstrapConfiguration : Located property source: CompositePropertySource {name='configService', propertySources=[MapPropertySource {name='configClient'}, MapPropertySource {name='http://localhost:3000/longqinsi/demo-config.git/user-service.yml'}, MapPropertySource {name='http://localhost:3000/longqinsi/demo-config.git/application.yml'}]}
o.s.boot.SpringApplication : The following profiles are active: peer2
com.example.userservice.HelloController : Received heartbeat from user service at port 14105.
o.s.boot.SpringApplication : Started application in 0.299 seconds (JVM running for 10313.932)
o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application USER-SERVICE with eureka with status DOWN
com.netflix.discovery.DiscoveryClient : Shutting down DiscoveryClient ...
com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient
com.netflix.discovery.DiscoveryClient : Shutting down DiscoveryClient ...
com.example.userservice.HelloController : Received heartbeat from user service at port 14105.
com.netflix.discovery.DiscoveryClient : Unregistering ...
com.netflix.discovery.DiscoveryClient : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104 - deregister status: 200
com.netflix.discovery.DiscoveryClient : Completed shut down of DiscoveryClient
o.s.c.n.eureka.InstanceInfoFactory : Setting initial instance status as: STARTING
com.netflix.discovery.DiscoveryClient : Initializing Eureka in region us-east-1
c.n.d.provider.DiscoveryJerseyProvider : Using JSON encoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using JSON decoding codec LegacyJacksonJson
c.n.d.provider.DiscoveryJerseyProvider : Using XML encoding codec XStreamXml
c.n.d.provider.DiscoveryJerseyProvider : Using XML decoding codec XStreamXml
c.n.d.s.r.aws.ConfigClusterResolver : Resolving eureka endpoints via configuration
com.netflix.discovery.DiscoveryClient : Disable delta property : false
com.netflix.discovery.DiscoveryClient : Single vip registry refresh property : null
com.netflix.discovery.DiscoveryClient : Force full registry fetch : false
com.netflix.discovery.DiscoveryClient : Application is null : false
com.netflix.discovery.DiscoveryClient : Registered Applications size is zero : true
com.netflix.discovery.DiscoveryClient : Application version is -1: true
com.netflix.discovery.DiscoveryClient : Getting all instance registry info from the eureka server
com.netflix.discovery.DiscoveryClient : The response status is 200
com.netflix.discovery.DiscoveryClient : Starting heartbeat executor: renew interval is: 30
c.n.discovery.InstanceInfoReplicator : InstanceInfoReplicator onDemand update allowed rate per min is 4
com.netflix.discovery.DiscoveryClient : Discovery Client initialized at timestamp 1570780516317 with initial instances count: 6
o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application USER-SERVICE with eureka with status DOWN
com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1570780516320, current=DOWN, previous=STARTING]
com.netflix.discovery.DiscoveryClient : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104: registering service...
o.s.c.n.e.s.EurekaServiceRegistry : Registering application USER-SERVICE with eureka with status UP
com.netflix.discovery.DiscoveryClient : Saw local status change event StatusChangeEvent [timestamp=1570780516320, current=UP, previous=DOWN]
o.s.c.n.e.s.EurekaServiceRegistry : Unregistering application USER-SERVICE with eureka with status DOWN
o.s.c.n.e.s.EurekaServiceRegistry : Registering application USER-SERVICE with eureka with status UP
com.netflix.discovery.DiscoveryClient : DiscoveryClient_USER-SERVICE/eureka1:user-service:14104 - registration status: 204

Related

Why are my services not registering with Jaeger?

I have deployed two services in kubernetes (bare-metal k0s. To do some tracing on Jaeger Service A makes a call to Service B for a JWT authentication token - this works perfectly when I test in Visual Studio (Service A is able to retrieve token from Service B).
However when I execute the call in Postman now that I have deployed to k0S , I cannot see neither the services or any traces Jaeger UI :
Using guide from here I have configured the services as below :
Service A (Startup.cs)
public class Startup
{
private readonly IWebHostEnvironment _environment;
public Startup(IConfiguration configuration, IWebHostEnvironment environment)
{
Configuration = configuration;
_environment = environment;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ModelContext>(o => o.UseOracle(Configuration.GetConnectionString("OracleConn")));
services.AddCors(c =>
{
c.AddPolicy("AllowOrigin", options => options.AllowAnyOrigin());
});
services.AddTransient<IAsyncAccountService<SttmCustAccount>, AccountService>();
services.AddTransient<IAsyncLookupResponse, LookupService>();
services.AddOpenTracing();
services.AddSingleton<ITracer>(cli =>
{
var serviceName = cli.GetRequiredService<IWebHostEnvironment>().ApplicationName;
Environment.SetEnvironmentVariable("JAEGER_SERVICE_NAME", serviceName);
if (_environment.IsDevelopment())
{
Environment.SetEnvironmentVariable("JAEGER_AGENT_HOST", "localhost");
Environment.SetEnvironmentVariable("JAEGER_AGENT_PORT", "6831");
Environment.SetEnvironmentVariable("JAEGER_SAMPLER_TYPE", "const");
}
ILoggerFactory loggerFactory = cli.GetRequiredService<ILoggerFactory>();
ISampler sampler = new ConstSampler(sample: true);
ITracer tracer = new Tracer.Builder(serviceName)
.WithLoggerFactory(loggerFactory)
.WithSampler(sampler)
.Build();
GlobalTracer.Register(tracer);
return tracer;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Service B has the exact same configuration.
Jaeger operator is set up using NodePort with config as in this file :
[file][https://drive.google.com/file/d/1nAjQ2jHrLSZLWoV9vGb3l4b0M1_yTMTw/view?usp=sharing]
I have to tried to expose the operator as Traefik Ingress but failed and had to change to NodePort (Can this affect registering of services , if at all ?) :
This is the call in Service A that retrieves token from Service B :
[HttpGet]
public async Task<IActionResult> GetAccountAsync([FromQuery] AccountLookupRequest request)
{
string authenticationResponse = await GetTokenAsync();
if (authenticationResponse != null)
{
_logger.LogInformation("Called AccountLookup API");
List<AccountLookupResponse> res = new List<AccountLookupResponse>();
var tranRef = String.Format("{1}{2}{0}", Helpers.aa, Helpers.bb, DateTime.Now.ToString("yyMMddHHmmss"));
...
}
}
static async Task<string> GetTokenAsync()
{
var payload = "{\"Username\": \"golide\",\"Password\": \"It#XXXXX\"}";
Uri uri = new Uri("http://10.XXX.XXX.XXX:31306/users/authenticate");
HttpContent c = new StringContent(payload, Encoding.UTF8, "application/json");
AuthenticationResponse responseEntity = new AuthenticationResponse();
var response = string.Empty;
using (var client = new HttpClient())
{
HttpResponseMessage result = await client.PostAsync(uri, c);
if (result.IsSuccessStatusCode)
{
response = result.StatusCode.ToString();
// responseEntity = JsonConvert.DeserializeObject<AuthenticationResponse>(response);
}
}
return response;
}
In Postman context the call to http://10.170.XXX.XXX:30488/account?field4=3025202645050&field7=GENERIC01&field10=ABC076 will implicitly invoke the AuthenticationAPI at http://10.170.xxx.xxx:31306/users/authenticate
UPDATE
Container logs show that trace-id and span-id are being generated :
[02:54:03 INF] Executed action method
FlexToEcocash.Controllers.AccountController.GetAccountAsync (FlexToEcocash), returned result Microsoft.AspNetCore.Mvc.OkObjectResult in 416.2876ms.
[02:54:03 WRN] Event-Exception: Microsoft.AspNetCore.Mvc.BeforeActionResult
System.NullReferenceException: Object reference not set to an instance of an object.
at System.Object.GetType()
at OpenTracing.Contrib.NetCore.AspNetCore.MvcEventProcessor.ProcessEvent(String eventName, Object arg)
at OpenTracing.Contrib.NetCore.AspNetCore.AspNetCoreDiagnostics.OnNext(String eventName, Object untypedArg)
at OpenTracing.Contrib.NetCore.Internal.DiagnosticListenerObserver.System.IObserver<System.Collections.Generic.KeyValuePair<System.String,System.Object>>.OnNext(KeyValuePair`2 value)
[02:54:03 INF] Executing ObjectResult, writing value of type 'FlexToEcocash.Models.ResponseModels.AccountLookupResponse'.
[02:54:03 INF] Executed action FlexToEcocash.Controllers.AccountController.GetAccountAsync (FlexToEcocash) in 421.8383ms
[02:54:03 WRN] Span has already been finished; will not be reported again. Operation: HTTP GET Trace Id: c976e25bf21bfae5 Span Id: c976e25bf21bfae5
[02:54:03 INF] Executed endpoint 'FlexToEcocash.Controllers.AccountController.GetAccountAsync (FlexToEcocash)'
[02:54:03 INF] Request finished in 426.0567ms 200 application/json; charset=utf-8
[02:54:03 WRN] Span has already been finished; will not be reported again. Operation: HTTP GET Trace Id: c976e25bf21bfae5 Span Id: c976e25bf21bfae5
What am I missing ?

Why is the REST endpoint being accessed twice, and can the second access be eliminated?

The code "works", in that it returns the expected information (a list of DemoPOJO objects). However, as demonstrated by the console output shown below, two calls are being made to the REST service at localhost:8080/v2/DemoPOJO. I have a feeling that the second call is a result of a lack of understanding of reactive programming, but I do not see where the second call is being made on this REST API, and would like to eliminate it as the redundancy is likely to be a performance issue when "something real" is deployed.
In the code provided, a call is made on localhost:8080/v3/DemoClient (implemented in DemoClientHandler), which then uses a WebClient object to access a corresponding REST service at localhost:8080/v2/DemoPOJO (implemented in DemoPOJOHandler).
(I have stripped the code down to only those statements associated with the REST endpoint /v3/DemoClient)
2019-09-26 12:30:23.389 INFO 4260 --- [ main] com.test.demo.DemoApplication : Starting DemoApplication on M7730-LFR with PID 4260 (D:\sandbox\DemoReactive\build\classes\java\main started by LesR in D:\sandbox\DemoReactive)
2019-09-26 12:30:23.391 INFO 4260 --- [ main] com.test.demo.DemoApplication : No active profile set, falling back to default profiles: default
2019-09-26 12:30:24.570 INFO 4260 --- [ main] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
2019-09-26 12:30:24.573 INFO 4260 --- [ main] com.test.demo.DemoApplication : Started DemoApplication in 1.41 seconds (JVM running for 1.975)
2019-09-26 12:30:28.796 INFO 4260 --- [ctor-http-nio-3] m.n.d.accesslogger.ServiceRequestLogger :
***** Begin, Access Request Log
Service request -> GET # http://localhost:8080/v3/DemoClient
Service handled by -> com.test.demo.democlient.DemoClientHandler.getAll()
***** End, Access Request Log
2019-09-26 12:30:28.823 INFO 4260 --- [ctor-http-nio-8] m.n.d.accesslogger.ServiceRequestLogger :
***** Begin, Access Request Log
Service request -> GET # http://localhost:8080/v2/DemoPOJO
Service handled by -> com.test.demo.demopojo.DemoPOJOHandler.getAll()
***** End, Access Request Log
2019-09-26 12:30:28.911 INFO 4260 --- [ctor-http-nio-9] m.n.d.accesslogger.ServiceRequestLogger :
***** Begin, Access Request Log
Service request -> GET # http://localhost:8080/v2/DemoPOJO
Service handled by -> com.test.demo.demopojo.DemoPOJOHandler.getAll()
***** End, Access Request Log
"Second-level" handler, access "first-level" REST API via WebClient (DemoClient)
#Component
public class DemoClientHandler {
public static final String PATH_VAR_ID = "id";
#Autowired
ServiceRequestLogger svcRequestLogger;
#Autowired
DemoClient demoClient;
public Mono<ServerResponse> getAll(ServerRequest request) {
Flux<DemoPOJO> fluxDemoPOJO = demoClient.getAll();
svcRequestLogger.logServiceRequest(this.getClass(), "getAll()", request);
return fluxDemoPOJO.hasElements().flatMap(hasElement -> {
return hasElement ? ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(fluxDemoPOJO, DemoPOJO.class)
: ServerResponse.noContent().build();
});
}
}
Uses WebClient to access "first-level" REST API...
#Component
public class DemoClient {
private final WebClient client;
public DemoClient() {
client = WebClient.create();
}
public Flux<DemoPOJO> getAll() {
return client.get().uri("http://localhost:8080/v2/DemoPOJO")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.flatMapMany(response -> response.bodyToFlux(DemoPOJO.class));
}
)
"First-level" handler...
#Component
public class DemoPOJOHandler {
#Autowired
private ServiceRequestLogger svcRequestLogger;
#Autowired
private DemoPOJOService service;
public Mono<ServerResponse> getAll(ServerRequest request) {
Flux<DemoPOJO> fluxDemoPOJO = service.getAll();
svcRequestLogger.logServiceRequest(this.getClass(), "getAll()", request);
return fluxDemoPOJO.hasElements().flatMap(hasElement -> {
return hasElement ? ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(fluxDemoPOJO, DemoPOJO.class)
: ServerResponse.noContent().build();
});
}
}
Router for second-level (WebClient) REST API...
#Configuration
public class DemoClientRouter {
#Bean
public RouterFunction<ServerResponse> clientRoutes(DemoClientHandler requestHandler) {
return nest(path("/v3"),
nest(accept(APPLICATION_JSON),
RouterFunctions.route(RequestPredicates.GET("/DemoClient"), requestHandler::getAll)));
}
}
Router for first-level REST API...
#Configuration
public class DemoPOJORouter {
#Bean
public RouterFunction<ServerResponse> demoPOJORoute(DemoPOJOHandler requestHandler) {
return nest(path("/v2"),
nest(accept(APPLICATION_JSON),
RouterFunctions.route(RequestPredicates.GET("/DemoPOJO"), requestHandler::getAll)));
}
}
Following code is added for completeness of the example, but I doubt is involved in the behavior I want to isolate-and-remove.
Service layer, supports DemoPOJO operations...
#Component
public class DemoPOJOService {
#Autowired
private DemoPOJORepo demoPOJORepo;
public Flux<DemoPOJO> getAll() {
return Flux.fromArray(demoPOJORepo.getAll());
}
}
Simple mock-up of POJO/data to support exploration...
#Component
public class DemoPOJORepo {
private static final int NUM_OBJS = 5;
private static DemoPOJORepo demoRepo = null;
private Map<Integer, DemoPOJO> demoPOJOMap;
private DemoPOJORepo() {
initMap();
}
public static DemoPOJORepo getInstance() {
if (demoRepo == null) {
demoRepo = new DemoPOJORepo();
}
return demoRepo;
}
public DemoPOJO[] getAll() {
return demoPOJOMap.values().toArray(new DemoPOJO[demoPOJOMap.size()]);
}
private void initMap() {
demoPOJOMap = new TreeMap<Integer, DemoPOJO>();
for (int ndx = 1; ndx < (NUM_OBJS + 1); ndx++) {
demoPOJOMap.put(ndx, new DemoPOJO(ndx, "foo_" + ndx, ndx + 100));
}
}
}
Logs (SLF4J/(Logback*) client access of REST services to application's log...
#Component
public class ServiceRequestLogger {
Logger logger = LoggerFactory.getLogger(this.getClass());
public void logServiceRequest(Class serviceHandler, String serviceMethod, ServerRequest request) {
logger.info(buildLogMessage(serviceHandler, serviceMethod, request));
}
private String buildLogMessage(Class serviceHandler, String serviceMethod, ServerRequest request) {
StringBuilder logMessage = new StringBuilder();
/* <housekeeping code to build message to log */
return logMessage.toString();
}
}
Probably violating some rule, but I've tried rephrasing the question and tightening up the code example at Why is the handler for a REST endpoint being accessed twice, when accessed from a WebClient?.
For the mods, I'm trying to avoid excessive editing of the original question, which can make comments appear non-topical.

Autofac multitenancy not resolving type per tenant

I have an ASP.NET MVC/WebApi2 application where I use Autofac.Multitenant 3.1.1. I've setup a TenantIdentificationStrategy that identifies the tenant. I've also registered a type as InstancePerTenant. I have a tenant id for each customer and a special id for a background job where no context is present
The TenantIdentificationStrategy is invoked correctly and the id is found from the context, but the InstancePerTenant is only instatiated twice on boot: Once for the default lifetimescope (tenant is null) and once for the first tenant. If I log out and in with another tenant, the same type is reused and not a new one. I can see in the container, that a tenantlifetime scope is created per tenantid, but not 4 independent InstancePerTenant types.
My tenant id code is:
public class TenantIdentificationStrategy : ITenantIdentificationStrategy
{
public bool TryIdentifyTenant(out object tenantId)
{
tenantId = null;
try
{
var context = HttpContext.Current;
if (context == null)
{
tenantId = "jobservice";
}
else
{
if (context.User?.Identity != null && context.User.Identity.IsAuthenticated)
{
var claims = context.User as ClaimsPrincipal;
tenantId = claims.FindAll(c => c.Type == "cID").FirstOrDefault()?.Value;
}
}
}
catch (HttpException)
{
// Happens at app startup in IIS 7.0
}
return tenantId != null;
}
}
In Startup.cs - Configuration() I have (snippet):
var builder = new ContainerBuilder();
builder.RegisterAssemblyModules(AppDomain.CurrentDomain.GetAssemblies());
var container = builder.Build();
var tenantIdentifier = new TenantIdentificationStrategy();
mtContainer = new MultitenantContainer(tenantIdentifier, container);
And a registration module in a seperate assembly:
public class RegistrationModule : Autofac.Module
{
protected override void Load(ContainerBuilder builder)
{
......
builder.RegisterType<Office365ClientService>().As<IOffice365ClientService>().InstancePerDependency();
builder.RegisterType<Office365Service>().As<IOffice365Service>().InstancePerDependency();
builder.RegisterType<Office365ClientHttpProvider>().As<IHttpProvider>().InstancePerTenant();
......
}
}
The Office365ClientService has a dependency on IHttpProvider
Did I miss something?

Is it possible to instruct ServicePartitionClient to talk to a specific node in service fabric?

I have
public class HttpCommunicationClient : HttpClient, ICommunicationClient
{
public HttpCommunicationClient()
: base(new HttpClientHandler() { AllowAutoRedirect = false, UseCookies = false })
{
}
public HttpCommunicationClient(HttpMessageHandler handler)
: base(handler)
{
}
public HttpCommunicationClient(HttpMessageHandler handler, bool disposeHandler)
: base(handler, disposeHandler)
{
}
#region ICommunicationClient
string ICommunicationClient.ListenerName { get; set; }
ResolvedServiceEndpoint ICommunicationClient.Endpoint { get; set; }
ResolvedServicePartition ICommunicationClient.ResolvedServicePartition { get; set; }
#endregion ICommunicationClient
}
and
public class HttpCommunicationClientFactory : CommunicationClientFactoryBase<HttpCommunicationClient>
{
private readonly Func<HttpCommunicationClient> _innerDispatcherProvider;
public HttpCommunicationClientFactory(IServicePartitionResolver servicePartitionResolver = null, IEnumerable<IExceptionHandler> exceptionHandlers = null, string traceId = null)
: this(() => new HttpCommunicationClient(), servicePartitionResolver, exceptionHandlers, traceId)
{
}
public HttpCommunicationClientFactory(Func<HttpCommunicationClient> innerDispatcherProvider, IServicePartitionResolver servicePartitionResolver = null, IEnumerable<IExceptionHandler> exceptionHandlers = null, string traceId = null)
: base(servicePartitionResolver, exceptionHandlers, traceId)
{
if (innerDispatcherProvider == null)
{
throw new ArgumentNullException(nameof(innerDispatcherProvider));
}
_innerDispatcherProvider = innerDispatcherProvider;
}
protected override void AbortClient(HttpCommunicationClient dispatcher)
{
if (dispatcher != null)
{
dispatcher.Dispose();
}
}
protected override Task<HttpCommunicationClient> CreateClientAsync(string endpoint, CancellationToken cancellationToken)
{
var dispatcher = _innerDispatcherProvider.Invoke();
dispatcher.BaseAddress = new Uri(endpoint, UriKind.Absolute);
return Task.FromResult(dispatcher);
}
protected override bool ValidateClient(HttpCommunicationClient dispatcher)
{
return dispatcher != null && dispatcher.BaseAddress != null;
}
protected override bool ValidateClient(string endpoint, HttpCommunicationClient dispatcher)
{
return dispatcher != null && dispatcher.BaseAddress == new Uri(endpoint, UriKind.Absolute);
}
}
and is using it like below
var servicePartitionClient = new ServicePartitionClient<HttpCommunicationClient>(_httpClientFactory,
_options.ServiceUri,
_options.GetServicePartitionKey?.Invoke(context),
_options.TargetReplicaSelector,
_options.ListenerName,
_options.OperationRetrySettings);
using (var responseMessage = await servicePartitionClient.InvokeWithRetryAsync(httpClient => ExecuteServiceCallAsync(httpClient, context)))
{
await responseMessage.CopyToCurrentContext(context);
}
The question is now, if I know at the time of using ServicePartitionClient that I would like it to connect to a specific node, is there any way to do so?
The case is that its a gateway application that forward requests to other services and I would like it to behave like with sticky sessions.
It makes more sense to think in terms of services than nodes. So rather than connecting to a specific node, you're actually connecting to a specific instance of a service.
When you're connecting to a service, if it's stateless, it shouldn't matter which instance you connect to, by definition of it being stateless. If you find that a user is tied to a specific instance of a service, that service is stateful (it's keeping track of some user state), and that's exactly the type of scenario that stateful services are meant to handle.
I found a solution, where I in the ExecuteServiceCallAsync call below reads a cookie from request with the information about which node it was connected to if its a sticky session, and if no cookie is present i set one with the information from the request. If the node dont exist any more the cookie is updated to new node.
using (var responseMessage = await servicePartitionClient.InvokeWithRetryAsync(httpClient => ExecuteServiceCallAsync(httpClient, context)))
{
await responseMessage.CopyToCurrentContext(context);
}

JBossMQ message redelivery + DLQ

I'm trying some scenarios with JMS and JBoss 4.2.2 and I have few problems with it.
I have a Queue
<mbean code="org.jboss.mq.server.jmx.Queue" name="jboss.mq.destination:service=Queue,name=notificationQueue">
<attribute name="JNDIName">jms.queue.testQueue</attribute>
<depends optional-attribute-name="DestinationManager">jboss.mq:service=DestinationManager</depends>
<depends optional-attribute-name="SecurityManager">jboss.mq:service=SecurityManager</depends>
<attribute name="SecurityConf">
<security>
<role name="testUser" read="true" write="true" />
</security>
</attribute>
</mbean>
and
<invoker-proxy-binding>
<name>message-driven-bean</name>
<invoker-mbean>default</invoker-mbean>
<proxy-factory>org.jboss.ejb.plugins.jms.JMSContainerInvoker</proxy-factory>
<proxy-factory-config>
<JMSProviderAdapterJNDI>DefaultJMSProvider</JMSProviderAdapterJNDI>
<ServerSessionPoolFactoryJNDI>StdJMSPool</ServerSessionPoolFactoryJNDI>
<CreateJBossMQDestination>true</CreateJBossMQDestination>
<MinimumSize>1</MinimumSize>
<MaximumSize>15</MaximumSize>
<MaxMessages>16</MaxMessages>
<MDBConfig>
<ReconnectIntervalSec>10</ReconnectIntervalSec>
<DLQConfig>
<DestinationQueue>queue/DLQ</DestinationQueue>
<MaxTimesRedelivered>3</MaxTimesRedelivered>
<TimeToLive>0</TimeToLive>
<DLQUser>jbossmquser</DLQUser>
<DLQPassword>letmein</DLQPassword>
</DLQConfig>
</MDBConfig>
</proxy-factory-config>
</invoker-proxy-binding>
To test redelivery I wrote MessageListener
import java.util.*;
import javax.jms.*;
import javax.naming.*;
public class NotifyQueueMessageListener {
public static void main(String[] args) throws NamingException, JMSException {
Hashtable<String, String> contextProperties = new Hashtable<String, String>();
contextProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
contextProperties.put(Context.PROVIDER_URL, "jnp://localhost:7099");
InitialContext initContext = new InitialContext(contextProperties);
Queue queue = (Queue) initContext.lookup("jms.queue.testQueue");
QueueConnection queueConnection = null;
try {
QueueConnectionFactory connFactory = (QueueConnectionFactory) initContext.lookup("ConnectionFactory");
queueConnection = connFactory.createQueueConnection("jbossmquser", "letmein");
Session queueSession = queueConnection.createSession(false, Session.CLIENT_ACKNOWLEDGE);
queueConnection.setExceptionListener(new MyExceptionListener());
MessageConsumer consumer = queueSession.createConsumer(queue);
MyMessageListener messageListener = new MyMessageListener();
consumer.setMessageListener(messageListener);
queueConnection.start();
Object o = new Object();
synchronized (o) {
o.wait();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("closing connection");
if (queueConnection != null) {
queueConnection.close();
}
}
}
static class MyMessageListener implements MessageListener {
#Override
public void onMessage(Message message) {
if (message instanceof ObjectMessage) {
ObjectMessage om = (ObjectMessage) message;
try {
System.out.printf("MyMessageListener.onMessage( %s ), %s\n\n", om, om.getObject());
boolean throwException = om.getBooleanProperty("throw");
if (throwException) {
System.out.println("throwing exception");
throw new NullPointerException("just for testing");
}
message.acknowledge();
} catch (JMSException jmse) {
jmse.printStackTrace();
}
}
}
}
static class MyExceptionListener implements ExceptionListener {
#Override
public void onException(JMSException jmse) {
jmse.printStackTrace();
}
}
}
and MessageSender
import java.text.*;
import java.util.*;
import javax.jms.*;
import javax.naming.*;
public class MessageSender {
public static void main(String[] args) throws NamingException, JMSException {
Hashtable<String, String> contextProperties = new Hashtable<String, String>();
contextProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
contextProperties.put(Context.PROVIDER_URL, "jnp://localhost:7099");
InitialContext initContext = new InitialContext(contextProperties);
Queue queue = (Queue) initContext.lookup("notificationQueue");
QueueConnection queueConnection = null;
try {
QueueConnectionFactory connFactory = (QueueConnectionFactory) initContext.lookup("ConnectionFactory");
queueConnection = connFactory.createQueueConnection("jbossmquser", "letmein");
// QueueSession queueSession = queueConnection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
// QueueSession queueSession = queueConnection.createQueueSession(true, Session.SESSION_TRANSACTED);
QueueSession queueSession = queueConnection.createQueueSession(false, Session.CLIENT_ACKNOWLEDGE);
// QueueSession queueSession = queueConnection.createQueueSession(false, Session.DUPS_OK_ACKNOWLEDGE);
QueueSender sender = queueSession.createSender(queue);
ObjectMessage message = queueSession.createObjectMessage();
message.setBooleanProperty("throw", true); // to throw exception in listener
message.setJMSDeliveryMode(DeliveryMode.NON_PERSISTENT);
message.setIntProperty("JMS_JBOSS_REDELIVERY_LIMIT", 3);
sender.send(message);
} finally {
System.out.println("closing connection");
if (queueConnection != null) {
queueConnection.close();
}
}
}
}
Expected behavior
Because I'm throwing Exception in onMessage() I expect that message will tried again several times (<MaxTimesRedelivered>3</MaxTimesRedelivered>) and after that it will be moved to DLQ, but it's not.
What I tried
I tried all acknowledge modes (AUTO, CLIENT, DUPS_OK) together with commiting, acknowledging but nothing worked, even message wasn't sent again.
I have no idea what's wrong. There is nothing relevant in JBoss logs.
When I try to stop and run again MesageListener I'm getting:
MyMessageListener.onMessage( org.jboss.mq.SpyObjectMessage {
Header {
jmsDestination : QUEUE.notificationQueue
jmsDeliveryMode : 2
jmsExpiration : 0
jmsPriority : 4
jmsMessageID : ID:13-13577584629501
jmsTimeStamp : 1357758462950
jmsCorrelationID: 20130109200742
jmsReplyTo : null
jmsType : null
jmsRedelivered : true
jmsProperties : {JMSXDeliveryCount=7, throw=true, JMS_JBOSS_REDELIVERY_LIMIT=3, JMS_JBOSS_REDELIVERY_COUNT=6}
jmsPropReadWrite: false
msgReadOnly : true
producerClientId: ID:13
}
} ), my message (2013-01-09 20:07:42)
MyMessageListener.onMessage( org.jboss.mq.SpyObjectMessage {
Header {
jmsDestination : QUEUE.notificationQueue
jmsDeliveryMode : 2
jmsExpiration : 0
jmsPriority : 4
jmsMessageID : ID:15-13577584942741
jmsTimeStamp : 1357758494274
jmsCorrelationID: 20130109200814
jmsReplyTo : null
jmsType : null
jmsRedelivered : true
jmsProperties : {JMSXDeliveryCount=6, throw=true, JMS_JBOSS_REDELIVERY_LIMIT=3, JMS_JBOSS_REDELIVERY_COUNT=5}
jmsPropReadWrite: false
msgReadOnly : true
producerClientId: ID:15
}
} ), my message (2013-01-09 20:08:14)
MyMessageListener.onMessage( org.jboss.mq.SpyObjectMessage {
Header {
jmsDestination : QUEUE.notificationQueue
jmsDeliveryMode : 2
jmsExpiration : 0
jmsPriority : 4
jmsMessageID : ID:20-13577586971991
jmsTimeStamp : 1357758697199
jmsCorrelationID: 20130109201137
jmsReplyTo : null
jmsType : null
jmsRedelivered : true
jmsProperties : {JMSXDeliveryCount=2, throw=true, JMS_JBOSS_REDELIVERY_LIMIT=3, JMS_JBOSS_REDELIVERY_COUNT=1}
jmsPropReadWrite: false
msgReadOnly : true
producerClientId: ID:20
}
} ), my message (2013-01-09 20:11:37)
MyMessageListener.onMessage( org.jboss.mq.SpyObjectMessage {
Header {
jmsDestination : QUEUE.notificationQueue
jmsDeliveryMode : 2
jmsExpiration : 0
jmsPriority : 4
jmsMessageID : ID:21-13577587683201
jmsTimeStamp : 1357758768320
jmsCorrelationID: 20130109201248
jmsReplyTo : null
jmsType : null
jmsRedelivered : true
jmsProperties : {JMSXDeliveryCount=2, throw=true, JMS_JBOSS_REDELIVERY_LIMIT=3, JMS_JBOSS_REDELIVERY_COUNT=1}
jmsPropReadWrite: false
msgReadOnly : true
producerClientId: ID:21
}
} ), my message (2013-01-09 20:12:48)
as you can see I tried also JMS_JBOSS_REDELIVERY_LIMIT.
Any idea?
I found very helpful post
https://community.jboss.org/wiki/ThrowingExceptionsFromAnMDB
that reads:
What type of Exceptions should an MDB throw?
The quick answer is none.
When I used transaction and createQueueSession(true, Session.SESSION_TRANSACTED) it worked just fine (redelivery and also DLQ).