Skip to content

bug: EtcdServiceRegistry stores timestamp as String instead of proper date format #293

@sfloess

Description

@sfloess

Bug Description

The service metadata timestamp is stored as a String representation of System.currentTimeMillis() without timezone information or standard date format, making it difficult to parse, compare, or interpret.

Location

jplatform-registry-etcd/src/main/java/org/flossware/jplatform/registry/etcd/EtcdServiceRegistry.java:104

Problematic Code

Map<String, String> metadata = new HashMap<>();
metadata.put("interface", serviceInterface.getName());
metadata.put("implementation", implementation.getClass().getName());
metadata.put("timestamp", String.valueOf(System.currentTimeMillis()));  // Just milliseconds!

String json = mapper.writeValueAsString(metadata);

Impact

  • No timezone: Milliseconds since epoch, no indication of timezone
  • Not human-readable: "1737907200000" not meaningful to humans
  • Hard to parse: Consumers must know it's Unix milliseconds
  • No standard format: Not ISO-8601 or any other standard
  • Comparison issues: String comparison doesn't work for times
  • Sorting problems: Can't sort chronologically without parsing

Example

// Current metadata in etcd:
{
  "interface": "com.example.MyService",
  "implementation": "com.example.MyServiceImpl",
  "timestamp": "1737907200000"
}

// Problems:
// 1. What timezone? (It's UTC but not indicated)
// 2. What does 1737907200000 mean? (Jan 26, 2025 but not obvious)
// 3. How to filter services registered in last hour?
//    - Must parse to long, subtract, compare
// 4. How to display to user?
//    - Must format programmatically

// When debugging in etcd:
$ etcdctl get /jplatform/services/MyService/abc123
{"timestamp":"1737907200000", ...}
// What time is that?
// Need calculator or code to convert

// When querying recent services:
// Can't do: SELECT * WHERE timestamp > '2025-01-26T00:00:00Z'
// Must do: Parse string, compare longs

Proposed Fix

Option 1 - Use ISO-8601 format:

import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

private static final DateTimeFormatter ISO_FORMATTER = 
    DateTimeFormatter.ISO_INSTANT.withZone(ZoneOffset.UTC);

Map<String, String> metadata = new HashMap<>();
metadata.put("interface", serviceInterface.getName());
metadata.put("implementation", implementation.getClass().getName());
metadata.put("timestamp", ISO_FORMATTER.format(Instant.now()));
// Result: "2025-01-26T12:00:00Z"

String json = mapper.writeValueAsString(metadata);

Option 2 - Store both formats:

Instant now = Instant.now();

Map<String, String> metadata = new HashMap<>();
metadata.put("interface", serviceInterface.getName());
metadata.put("implementation", implementation.getClass().getName());
metadata.put("timestamp", ISO_FORMATTER.format(now));  // Human-readable
metadata.put("timestamp_millis", String.valueOf(now.toEpochMilli()));  // For programmatic use

String json = mapper.writeValueAsString(metadata);

Option 3 - Use structured metadata object:

public static class ServiceMetadata {
    @JsonProperty("interface")
    private String serviceInterface;
    
    @JsonProperty("node_id")
    private String nodeId;
    
    @JsonProperty("registered_at")
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
    private Instant registeredAt;
    
    public ServiceMetadata(String serviceInterface, String nodeId) {
        this.serviceInterface = serviceInterface;
        this.nodeId = nodeId;
        this.registeredAt = Instant.now();
    }
    
    // Getters
}

// In registerService():
ServiceMetadata metadata = new ServiceMetadata(
    serviceInterface.getName(),
    getNodeId()
);

String json = mapper.writeValueAsString(metadata);
// Result: {"interface":"...","node_id":"...","registered_at":"2025-01-26T12:00:00.000Z"}

Option 4 - Configure ObjectMapper to handle dates properly:

public EtcdServiceRegistry(EtcdRegistryConfig config) {
    this.config = config;
    this.localServices = new ConcurrentHashMap<>();
    this.registeredServices = new ConcurrentHashMap<>();
    
    // Configure ObjectMapper for proper date handling
    this.mapper = new ObjectMapper();
    this.mapper.registerModule(new JavaTimeModule());
    this.mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    this.mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"));
}

// Then use Instant in metadata:
Map<String, Object> metadata = new HashMap<>();  // Object, not String
metadata.put("interface", serviceInterface.getName());
metadata.put("timestamp", Instant.now());  // Will be serialized as ISO-8601

String json = mapper.writeValueAsString(metadata);

Recommendation: Option 3 - use structured metadata object with proper date field. This is type-safe, standard, and human-readable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions